ScreenshotNeo

BlogHow-to

Convert HTML to PNG in a Next.js App

Generate PNG graphics in Next.js with ImageResponse, or use a browser-based screenshot API when you need to capture arbitrary HTML.

By the ScreenshotNeo team4 October 20269 min read

Short answer: If your image can be built from JSX and the CSS supported by Next.js ImageResponse, use it to generate a PNG. If you need a faithful screenshot of an arbitrary, browser-rendered page, ImageResponse is not a general-purpose browser screenshot engine; use a browser-rendering implementation compatible with your runtime, or call a screenshot API.

This guide shows a runnable App Router endpoint using ImageResponse, explains its limits and deployment considerations, and covers when a browser screenshot is the better fit. The examples use the current Next.js App Router conventions; check the official guide for details that vary by your installed version.

1. Choose the right rendering approach

Need Approach What to check
A designed graphic, card, badge, or Open Graph image controlled by your app ImageResponse Your JSX and CSS must fit its supported subset.
A screenshot of an arbitrary URL or complex existing page Browser-based rendering or a screenshot API Browser fidelity, authentication state, host support for browser binaries and filesystem, and execution time.
An image that can be produced before deployment and does not need request-time data Precompute or generate it as a static asset Whether the content changes often enough to justify runtime rendering.

Next.js documents ImageResponse as using @vercel/og, Satori, and resvg. It generates PNG from JSX rather than capturing a full browser tab. Its CSS support includes flexbox and a subset of properties; advanced layouts such as CSS Grid are unsupported. See the Next.js Metadata and OG images guide.

For arbitrary HTML, first decide whether “convert” means generating a graphic from controlled markup or reproducing the actual browser appearance of a page. That distinction determines the renderer, runtime, and whether the page’s browser-only state can be accessed.

2. Generate a PNG with ImageResponse

Create a Route Handler at app/api/og/route.tsx. This example accepts a title query parameter, constrains its length, and returns a PNG. It uses only flexbox and inline styles supported by the image renderer.

// app/api/og/route.tsx
import { ImageResponse } from 'next/og'

export const runtime = 'edge'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const title = (searchParams.get('title') ?? 'A generated image').slice(0, 120)

  return new ImageResponse(
    (
      <div
        style={{
          width: '100%',
          height: '100%',
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'center',
          padding: 64,
          background: '#101827',
          color: '#ffffff',
          fontSize: 64,
          fontWeight: 700,
        }}
      >
        <div style={{ color: '#93c5fd', fontSize: 24, marginBottom: 24 }}>
          NEXT.JS IMAGE</div>
        <div>{title}</div>
      </div>
    ),
    { width: 1200, height: 630 }
  )
}

Run the Next.js app and open /api/og?title=Hello%20world. The response is a PNG image. The handler is an endpoint, so callers can also request it from code:

curl -o og.png 'http://localhost:3000/api/og?title=Hello%20world'

Route Handlers live in the app directory, use the Web Request and Response APIs, and can return images or files. They are not cached by default. If you opt a GET handler into caching, do so only when the resulting image is stable and safe to share. Review the Route Handlers guide.

Use parameters safely

  • Validate and bound query values. This example caps the title length; production handlers may also need to reject empty or disallowed values.
  • Do not put secrets or sensitive page data in a public image URL. Route Handlers are public unless you add authentication and authorization.
  • Do not return stack traces or private upstream error details to callers.
  • If output depends on a user, tenant, or secret, ensure caching cannot serve one caller’s image to another.

3. Fonts, styles, and layout limits

Use the CSS subset documented for the Next.js version installed in your project. The official guide says: “Only flexbox and a subset of CSS properties are supported. Advanced layouts (e.g. display: grid) will not work.” Do not assume a page that renders correctly in Chrome will render identically with ImageResponse.

For custom fonts, follow the current guide’s font-loading example and verify the font format and loading method against your installed version and runtime. Keep layout deliberate: use flexbox, explicit dimensions, spacing, and colors, and test long text, missing values, and different character sets. If your design relies on grid, complex selectors, browser APIs, CSS animations, or a large existing stylesheet, it may need to be simplified or rendered in a browser instead.

4. Capture arbitrary HTML in a browser

When the requirement is a real browser screenshot, a browser renderer must load and paint the page. The reviewed Next.js documentation does not establish that ImageResponse provides arbitrary-page browser fidelity, and this guide does not claim a particular browser automation package has been tested. Select a browser-rendering implementation only after checking its current compatibility with your Next.js version and deployment host.

  1. Identify the page context. Determine whether the target is a public URL, an app route, or content requiring cookies, authorization, or client-side state.
  2. Choose where rendering runs. A browser may require binaries, filesystem access, memory, and time. Verify the host supports those requirements and its request duration limits.
  3. Define the output contract. Decide dimensions, full-page versus viewport capture, image format, and how errors are returned.
  4. Protect the endpoint. Validate target URLs and enforce access controls. A public endpoint that screenshots caller-supplied URLs can be abused to make server-side requests; restrict destinations and avoid exposing secrets.
  5. Test representative pages. Include slow pages, long pages, pages with lazy images, authenticated state if needed, and failures. Establish timeouts and a clear error response.

These are decision and security considerations, not claims about the behavior of a specific browser package. Check that package’s documentation and your host’s current deployment limits before shipping.

5. Runtime, routes, caching, and deployment

Route Handlers support the standard HTTP methods and can serve image or file content. They are not cached by default, though GET handlers can opt into caching through route configuration. Static export has no runtime server: runtime-dependent features are unsupported, and only statically configured GET Route Handlers are supported in export mode. See the Next.js Backend for Frontend guide.

  • Server endpoint: Use a Route Handler when a client or external caller needs to request a generated image. Treat it as public unless you add access controls.
  • Client-only data: Next.js describes Client Components as the place for interactivity and browser-only APIs. If rendering depends on browser state, keep that work in a browser context or pass appropriate data to a server renderer.
  • Lambda hosts: Some hosts run handlers as lambdas. Requests may not share in-memory state, filesystem writes may be unavailable, and long-running work can be terminated on timeout. Check your provider’s current limits before bundling a browser or starting long renders.
  • Static export: There is no runtime server to handle arbitrary request-time rendering. Precompute images or use a separate runtime service if output must be generated on demand.
  • Cache policy: Cache only when the image is deterministic for the cache key and safe for all callers who can receive it. Include every output-affecting input in that key.

6. Request an image from common clients

For the sample endpoint above, the following requests fetch the generated PNG. Replace the local origin with your deployed app URL.

cURL

curl --fail --show-error \
  --output og.png \
  'https://your-app.example/api/og?title=Hello%20world'

Python

import requests

url = 'https://your-app.example/api/og'
response = requests.get(url, params={'title': 'Hello world'}, timeout=30)
response.raise_for_status()
with open('og.png', 'wb') as image_file:
    image_file.write(response.content)

Node.js

const url = new URL('https://your-app.example/api/og')
url.searchParams.set('title', 'Hello world')

const response = await fetch(url)
if (!response.ok) {
  throw new Error(`Image request failed: ${response.status} ${response.statusText}`)
}
const bytes = new Uint8Array(await response.arrayBuffer())
await import('node:fs/promises').then(({ writeFile }) => writeFile('og.png', bytes))

7. Troubleshooting

Symptom Likely cause What to do
Image generation fails on a CSS property or layout The markup uses a property or layout outside the supported subset, such as CSS Grid. Check the installed version’s guide and reduce the design to supported styles, often flexbox; use a browser renderer if full CSS fidelity is required.
Text or font differs from the design The font-loading method, format, or font availability does not match the renderer/runtime. Follow the current Next.js font example, verify format support, and test the deployed runtime.
Works locally but fails after deployment The host has different runtime, binary, filesystem, memory, or duration constraints. Check the host’s current function limits and the renderer’s runtime requirements. Some lambda environments disallow filesystem writes or terminate long work.
Endpoint exposes private content The handler is reachable without the app’s expected authentication, or caching crosses user boundaries. Add authentication and authorization, validate inputs, and use a cache policy that cannot share private output.
Request returns 404 The route file is outside the app directory, has the wrong path, or the URL does not match the route. Confirm the file is app/api/og/route.tsx and request /api/og.
Static export has no generated response There is no runtime server to evaluate request-time code. Precompute the image or deploy the handler in a supported runtime server.
Image is stale or shows another caller’s data Caching was enabled without accounting for all output inputs or privacy boundaries. Disable caching for personalized output or ensure the cache key and access controls isolate it correctly.

8. Performance, reliability, and cost

For ImageResponse, keep the JSX tree and assets small, avoid unnecessary request-time work, and cache only deterministic, shareable output. For browser rendering, page load and render time depend on the target page and environment; the supplied Next.js sources provide no benchmark or universal latency figure. Bound work with a timeout and make the endpoint return a controlled error if rendering cannot finish.

Reliability depends on the full path: app runtime, host limits, font and asset availability, and any external page or service being rendered. For expensive or long work, consider whether the image can be precomputed or generated asynchronously in an architecture suited to the chosen host. Verify current provider limits rather than relying on a generic timeout or memory assumption.

Next.js does not specify a universal per-image rendering cost in the cited material. Account for the hosting plan and any third-party browser-rendering service you choose. Compare the ongoing runtime and maintenance burden with static generation when output does not need to change per request.

9. Or skip the browser setup

If you need a browser screenshot rather than a graphic composed from JSX, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns an image or PDF; see the ScreenshotNeo API documentation for parameters and response details.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' })
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`)
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`)
const bytes = new Uint8Array(await res.arrayBuffer())
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', bytes))
  • Cookie banners are accepted and removed before capture, alongside 60+ known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off.
  • Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed. Responses identify the page verdict and billing state in headers.
  • An MCP server lets AI agents, including Claude, Cursor, and other MCP clients, use screenshot tools.
  • The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is on every plan.

Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.

10. FAQ

Can ImageResponse convert an existing HTML file directly?

It renders JSX with a supported CSS subset. If the requirement is to reproduce an existing HTML page as a browser would paint it, choose a browser-rendering solution and verify its runtime compatibility.

Can I use this route for social preview images?

Yes. Dynamic image generation is a documented use case for Next.js image responses. Ensure the route is reachable by the systems that request previews and that its output does not depend on inaccessible user state.

Should every generated image be cached?

No. Cache only when the output is stable for the cache key and safe to share. Personalized output needs a policy that prevents cross-user reuse.

Does this work with static export?

Static export has no runtime server, so arbitrary request-time rendering is unavailable. Use statically configured output or generate the asset before deployment.

Sources