How to Take Website Screenshots in Next.js

Profile

Written By Hanzala Saleem

Updated At June 18, 2026 | 7 min read

To take a website screenshot in Next.js, send the target URL to a screenshot API from a server-side route and render the returned image. This avoids running a headless browser inside your own deployment, which fails on serverless hosts like Vercel because Chromium cannot launch in that runtime.

This guide builds a working Next.js app step by step. You enter a website URL, click a button, and the app displays a screenshot of that page. The app uses the App Router (the default in current Next.js) and calls ScreenshotAPI from a route handler, so your API key never reaches the browser.

We also cover the two questions every developer hits next: how to run this on Vercel without a server crash, and when a screenshot API is the right call versus self-hosting Puppeteer.

What is the fastest way to screenshot a website in Next.js?

The fastest way is to call a screenshot API from a Next.js route handler. You send a target URL to the API, it renders the page in a managed Chromium browser, and it returns an image URL or the image bytes. Your app displays the result. This takes one HTTP request and works on any host, including serverless platforms where you cannot launch a browser yourself. Self-hosted Puppeteer is the alternative, but it requires a Node.js runtime that can run Chromium and adds memory and maintenance overhead.

Key Takeaways

  • Build a Next.js app with a URL input that captures and displays a screenshot on demand.
  • Call the screenshot API from a server-side route handler so your API key stays off the client.
  • Ship to Vercel or any serverless host without the Chromium launch errors that break self-hosted Puppeteer.
  • Choose between a screenshot API and self-hosted Puppeteer based on scale, runtime, and maintenance cost.

What We'll Create

By the end of this tutorial, you will have built a Next.js app that:

  • Has an input field to take a website URL
  • Displays a screenshot of the website when the user clicks the "Capture Screenshot" button
  • Utilizes screenshotapi.net to capture and display website screenshots

Steps

Step 1: Set up the Next.js project

Create a new Next.js project. The current version uses the App Router by default, which is what this guide targets.

npx create-next-app@latest website-screenshot
cd website-screenshot

Accept the defaults when prompted. The key choice is the App Router (the app/ directory), which we rely on for the server-side route handler in Step 4.

Step 2: Decide whether you need an HTTP library

Next.js ships with the global fetch function on both the server and the client, so you do not need an extra HTTP library for this app. The examples below use fetch. If you prefer Axios for interceptors or a shared client config, install it:

npm install axios

The rest of this guide uses fetch to keep the dependency count at zero.

Step 3: Build the input field and capture button

Open app/page.js and replace its contents with a client component that holds the URL input and the capture button. The 'use client' directive is required because this component uses React state.

'use client';

import { useState } from 'react';

export default function Home() {
  const [url, setUrl] = useState('');
  const [screenshotUrl, setScreenshotUrl] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const handleCaptureScreenshot = async () => {
    // API call added in Step 4
  };

  return (
    <main style={{ maxWidth: 640, margin: '40px auto', padding: 16 }}>
      <input
        type="text"
        value={url}
        onChange={(e) => setUrl(e.target.value)}
        placeholder="Enter website URL (https://example.com)"
        style={{ width: '100%', padding: 8 }}
      />
      <button onClick={handleCaptureScreenshot} disabled={loading} style={{ marginTop: 12 }}>
        {loading ? 'Capturing...' : 'Capture Screenshot'}
      </button>
      {error && <p style={{ color: 'crimson' }}>{error}</p>}
    </main>
  );
}

The component tracks four pieces of state: the input URL, the returned screenshot URL, a loading flag for the button label, and an error message. We wire up the capture logic next.

Step 4: Add a server-side route that calls ScreenshotAPI

Register at ScreenshotAPI and copy your API key from the dashboard. Store it as an environment variable, never in client code. Create a .env.local file in your project root:

SCREENSHOT_API_TOKEN=your_api_key_here

Putting the token in a route handler keeps it on the server. If you call the API directly from the browser, anyone can read your key from the network tab and spend your quota.

Create the route handler at app/api/screenshot/route.js:

export async function POST(request) {
  const { url } = await request.json();

  if (!url) {
    return Response.json({ error: 'A url is required.' }, { status: 400 });
  }

  const token = process.env.SCREENSHOT_API_TOKEN;
  const endpoint =
    `https://shot.screenshotapi.net/v3/screenshot` +
    `?token=${token}&url=${encodeURIComponent(url)}&output=json&file_type=png`;

  try {
    const response = await fetch(endpoint);
    const data = await response.json();
    return Response.json({ screenshot: data.screenshot });
  } catch (err) {
    return Response.json({ error: 'Failed to capture the screenshot.' }, { status: 500 });
  }
}

Two details matter here. First, encodeURIComponent(url) escapes the target URL so query parameters in the user's input do not break the request. Second, output=json tells ScreenshotAPI to return a JSON object whose screenshot field holds the hosted image URL, which is what the front end renders in the next step.

Step 5: Call the route and display the screenshot

Fill in handleCaptureScreenshot to POST the URL to your route handler and store the returned image URL in state. Then render an img element when a screenshot exists.

'use client';

import { useState } from 'react';

export default function Home() {
  const [url, setUrl] = useState('');
  const [screenshotUrl, setScreenshotUrl] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const handleCaptureScreenshot = async () => {
    setLoading(true);
    setError('');
    setScreenshotUrl('');

    try {
      const res = await fetch('/api/screenshot', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ url }),
      });
      const data = await res.json();

      if (!res.ok) throw new Error(data.error);
      setScreenshotUrl(data.screenshot);
    } catch (err) {
      setError(err.message || 'Something went wrong.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <main style={{ maxWidth: 640, margin: '40px auto', padding: 16 }}>
      <input
        type="text"
        value={url}
        onChange={(e) => setUrl(e.target.value)}
        placeholder="Enter website URL (https://example.com)"
        style={{ width: '100%', padding: 8 }}
      />
      <button onClick={handleCaptureScreenshot} disabled={loading} style={{ marginTop: 12 }}>
        {loading ? 'Capturing...' : 'Capture Screenshot'}
      </button>
      {error && <p style={{ color: 'crimson' }}>{error}</p>}
      {screenshotUrl && (
        <img src={screenshotUrl} alt={`Screenshot of ${url}`} style={{ marginTop: 16, width: '100%' }} />
      )}
    </main>
  );
}

Run npm run dev, open http://localhost:3000, paste a URL, and click the button. The screenshot appears below the form. The loading flag swaps the button label while the capture is in flight, and the error state surfaces any failed request instead of failing silently.

Step 6: Deploy to Vercel without breaking

This app deploys to Vercel with no extra configuration because the heavy work happens inside ScreenshotAPI, not your function. The route handler only forwards a URL and returns JSON, so it stays well under serverless memory and execution limits.

This is the main reason to use a screenshot API in a Next.js project. Self-hosted Puppeteer launches a Chromium binary, which a Vercel or Cloudflare Workers function cannot run without bundling a special build, and even then, you fight cold starts and the 50 MB function size cap. Set SCREENSHOT_API_TOKEN in your Vercel project environment variables, deploy, and the app behaves the same as it does locally.

Screenshot API vs self-hosted Puppeteer in Next.js

Both approaches capture screenshots. They differ in where the browser runs and who maintains it.

FactorScreenshot APISelf-hosted Puppeteer
Runs on serverless (Vercel, Workers)Yes, no configNo, needs custom Chromium build or a VPS
Setup timeOne API callInstall Chromium, configure launch flags
Cold start and memoryHandled by the APILarge bundle, slow cold starts, high memory
Ad and cookie banner blockingBuilt inYou write and maintain it
MaintenanceNonePatch Chromium, fix zombie processes
Cost modelPer captureServer time plus engineering time
Best fitServerless apps, scale, low maintenanceFull control on a long-running Node server

Use self-hosted Puppeteer when you run a long-lived Node.js server, need full control of the browser, and capture low volumes. Use a screenshot API when you deploy serverless, want ad and banner blocking out of the box, or do not want to babysit a browser in production.

Why developers use ScreenshotAPI for this

  • One HTTP request per capture, so integration is a few lines in any Next.js route handler.
  • Output as PNG, JPG, WebP, or PDF, with custom viewport, full-page capture, and render delays.
  • Ad, cookie banner, and popup blocking built in, so captures are clean without manual selector config.
  • Managed Chromium rendering, so React, Vue, and Next.js pages render fully and you avoid the cold starts and memory limits of self-hosted Puppeteer on serverless.

Conclusion

You now have a Next.js app that captures and displays website screenshots, with the API call running server-side and the app deployable to Vercel without browser launch errors. From here you can add batch capture, expose output options like full-page or PDF, or persist results to your own storage.

Ready to build it for real? Create a free ScreenshotAPI key and drop it into the route handler from Step 4.

Frequently Asked Questions

Why does my Puppeteer screenshot code fail on Vercel?

Vercel functions run in a serverless environment that cannot launch a standard Chromium binary, so puppeteer.launch() throws at runtime. You either bundle a serverless-specific Chromium build and accept slow cold starts, or move the capture to a screenshot API that runs the browser for you. The API approach is why the app in this guide deploys to Vercel unchanged.

How do I keep my screenshot API key out of the browser?

Call the API from a server-side route handler, as this guide does in Step 4, and read the key from an environment variable. The browser only talks to your own /api/screenshot route, so the token never appears in client code or the network tab.

Can I capture screenshots of pages behind a login?

Yes. ScreenshotAPI accepts authentication parameters and cookies, so you can capture pages that require a session. Pass the credentials as request parameters from your route handler rather than the browser.

Can I customize the output format and viewport?

Yes. ScreenshotAPI supports PNG, JPG, WebP, and PDF output, custom viewport sizes, full-page capture, and a render delay for animation-heavy pages. Add the parameters to the endpoint URL in your route handler. See the API documentation for the full list.

How do I capture many screenshots on a schedule?

Trigger your route handler from a Vercel cron job or a scheduled serverless function, or use the scheduling features of the API directly. For large batches, send URLs through a queue so you do not block a single request.