ScreenshotNeo

BlogHow-to

How to Capture a Website Screenshot Quickly with PHP

Capture full-page or element screenshots in PHP with Browsershot, configure waits and viewports, troubleshoot Chrome, or use ScreenshotNeo.

By the ScreenshotNeo team1 October 20268 min read

The quickest PHP implementation is Spatie Browsershot. It gives PHP a short URL-to-image API, but it runs Puppeteer and headless Chrome or Chromium underneath, so those runtime dependencies must be installed on the server.

<?php

require __DIR__ . '/vendor/autoload.php';

use Spatie\Browsershot\Browsershot;

$path = __DIR__ . '/storage/example.png';

Browsershot::url('https://example.com')
    ->save($path);

echo "Saved to {$path}\n";

Browsershot documents PNG as the default image output. JPEG is available when you set the screenshot type and quality. See the image creation guide for the complete API.

1. Install the PHP and browser dependencies

Browsershot is a PHP wrapper around Puppeteer. Install the package with Composer, install the Node.js dependencies used by Puppeteer, and make a Chrome or Chromium binary available to the process. Laravel’s screenshot requirements also call out Node.js and Chrome/Chromium.

composer require spatie/browsershot
npm install puppeteer

Confirm that the user running PHP can execute Node and the browser. On shared hosting, containers with restricted sandboxing, or older PHP installations, the browser process may be the part that needs a different deployment plan.

2. Choose the capture area

Viewport screenshot

Browsershot::url('https://example.com')
    ->windowSize(1440, 900)
    ->save(__DIR__ . '/storage/viewport.png');

windowSize(width, height) fixes the browser viewport. This is useful when you need repeatable desktop output or want to reproduce a particular responsive breakpoint.

Full-page screenshot

Browsershot::url('https://example.com')
    ->fullPage()
    ->save(__DIR__ . '/storage/full-page.png');

fullPage() captures the page’s scrollable content rather than only the initial viewport. Very long pages can produce large images, so consider a narrower viewport, JPEG output, or a defined region when the image is destined for a report or thumbnail.

One element

Browsershot::url('https://example.com/pricing')
    ->select('.pricing-table')
    ->save(__DIR__ . '/storage/pricing.png');

Use select() when the page contains a stable CSS selector for the component you need. If the selector is generated dynamically, wait for it before capturing.

Clipped region

Browsershot::url('https://example.com')
    ->clip(0, 200, 1200, 700)
    ->save(__DIR__ . '/storage/region.png');

clip(x, y, width, height) defines a rectangular region in CSS pixels. Keep the clip inside the rendered page and verify the coordinates at the viewport size you use.

3. Set image format, density, and mobile behavior

Browsershot::url('https://example.com')
    ->windowSize(1280, 800)
    ->deviceScaleFactor(2)
    ->setScreenshotType('jpeg', 85)
    ->save(__DIR__ . '/storage/retina.jpg');
  • PNG: lossless and the documented default.
  • JPEG: smaller files for photographic or preview content; choose a quality value appropriate for your use case.
  • Device scale factor: increases pixel density and output dimensions while preserving the CSS viewport.
  • Mobile emulation: use Browsershot’s documented device emulation options when you need mobile layout, user agent, and device metrics together.

Record the URL, viewport, device scale factor, output type, and wait rule with each generated image if you need reproducible output.

4. Wait for JavaScript and lazy content

A screenshot can be taken before an application has rendered its data, fonts, charts, or lazy images. Pick a wait rule based on how the page behaves.

Wait for network idle

Browsershot::url('https://example.com/dashboard')
    ->waitUntilNetworkIdle()
    ->save(__DIR__ . '/storage/dashboard.png');

Network-idle waiting is useful after normal page requests finish. It can be unsuitable for pages with analytics, polling, sockets, or other continuing traffic; Browsershot also documents a less strict network-idle mode.

Wait for a selector

Browsershot::url('https://example.com/catalog')
    ->waitForSelector('.product-grid')
    ->fullPage()
    ->save(__DIR__ . '/storage/catalog.png');

Wait for a JavaScript condition

Browsershot::url('https://example.com/report')
    ->waitForFunction("document.querySelector('[data-rendered=\\\"true\\\"]')")
    ->save(__DIR__ . '/storage/report.png');

Use a fixed delay only when necessary

Browsershot::url('https://example.com')
    ->delay(1500)
    ->save(__DIR__ . '/storage/delayed.png');

A delay is simple but couples the result to a timing guess. Prefer a selector or page condition when the application exposes one. For lazy-loaded images, combine a full-page capture with a wait that reflects when the images become available.

5. A complete reusable PHP function

<?php

require __DIR__ . '/vendor/autoload.php';

use Spatie\Browsershot\Browsershot;

function captureScreenshot(
    string $url,
    string $output,
    int $width = 1440,
    int $height = 900,
    bool $fullPage = false
): void {
    $shot = Browsershot::url($url)
        ->windowSize($width, $height)
        ->waitUntilNetworkIdle();

    if ($fullPage) {
        $shot->fullPage();
    }

    $shot->save($output);
}

captureScreenshot(
    'https://example.com',
    __DIR__ . '/storage/example.png',
    1440,
    900,
    true
);

6. Keep credentials and private pages safe

Do not put service keys, session cookies, or authorization values directly in source code committed to a repository. Read secrets from environment variables or your deployment’s secret manager. For authenticated pages, configure the browser request with the headers or cookies supported by your chosen capture tool, and ensure the resulting files have restricted permissions.

For public pages, validate or allow-list incoming URLs before passing them to a screenshot endpoint. Otherwise, an internal application could be asked to fetch private network addresses. Apply request timeouts, output-size limits, and cleanup rules for generated files.

7. Alternatives when Browsershot is not the right fit

Approach Where the browser runs When to consider it
Browsershot Your Node.js and Chrome/Chromium runtime You want a concise PHP API and can manage those dependencies.
chrome-php/chrome Your Chrome/Chromium runtime You need more direct browser control from PHP.
Playwright PHP Your Playwright browser runtime You prefer Playwright’s browser automation model.
Hosted screenshot API The provider’s infrastructure Your hosting cannot run a browser or you want to remove browser installation and maintenance.

Compare these options by browser location, PHP and runtime prerequisites, capture controls, secret handling, host permissions, and whether sending page URLs to an external service is acceptable. The cited documentation does not establish a performance or cost winner among these categories.

8. Or skip the browser setup

ScreenshotNeo provides a hosted website screenshot API. One GET request returns PNG, JPEG, WebP, or PDF output, so PHP does not need Node.js, Puppeteer, or a Chrome binary.

See the ScreenshotNeo API documentation for options such as full-page capture with lazy images loaded, CSS selector capture, viewport and device presets, retina scale, waits, custom CSS and JavaScript, cookies and headers, PDF settings, caching, signed links, asynchronous jobs, bulk capture, and usage reporting.

<?php

$url = 'https://stripe.com';
$query = http_build_query([
    'access_key' => getenv('SCREENSHOTNEO_API_KEY'),
    'url' => $url,
]);

$ch = curl_init("https://api.screenshotneo.com/v1/shot?{$query}");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 90,
]);

$image = curl_exec($ch);
if ($image === false) {
    throw new RuntimeException(curl_error($ch));
}

$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status < 200 || $status >= 300) {
    throw new RuntimeException("ScreenshotNeo returned HTTP {$status}");
}

file_put_contents(__DIR__ . '/shot.webp', $image);
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}`);

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether the request was billed. ScreenshotNeo also has an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000.

Start with 1,000 free ScreenshotNeo screenshots per month.

9. Troubleshooting

Symptom Likely cause Fix
“Node not found” or a Puppeteer error Node.js is missing or not on PHP’s executable path. Install Node.js, install Puppeteer, and configure the process environment used by PHP-FPM, the queue worker, or the CLI.
Chrome executable cannot be found Chrome/Chromium is not installed or the process cannot access it. Install a supported browser and configure the browser path according to your Browsershot deployment.
Blank or partially rendered image The capture happened before JavaScript or lazy resources finished. Wait for a selector, JavaScript condition, or suitable network-idle state; inspect the page at the same viewport.
Timeout on an active application Long polling, analytics, or sockets prevent strict network idle. Use a less strict network-idle mode, a selector wait, or a bounded delay.
Element screenshot fails The selector is missing, duplicated, hidden, or created after initial load. Wait for the selector, use a stable selector, and confirm the element has non-zero dimensions.
Mobile layout is wrong Only the viewport width changed; device metrics or emulation were not applied. Use the documented mobile emulation option and verify the target device settings.
Permission denied writing the file The PHP user cannot write to the output directory. Create the directory and grant only the required write permission to the runtime user.
Large files or slow jobs Full-page, high-density PNG output contains many pixels. Reduce viewport or scale, select only the needed element, or use JPEG where quality permits.

10. Performance, reliability, and cost notes

  • Reuse the runtime: queue captures and avoid launching unnecessary concurrent browser processes on small hosts.
  • Choose the smallest capture: an element or clipped region usually needs less rendering and storage than a full-page image.
  • Wait on page state: deterministic selectors are generally easier to operate than arbitrary long delays.
  • Bound work: set process and HTTP timeouts, limit output dimensions, and clean old files.
  • Plan for dependencies: upgrades to PHP, Node.js, Puppeteer, Chrome, or the host image can change behavior; pin and review versions in deployment.
  • Hosted API accounting: confirm how a provider bills errors, cache hits, and successful captures. ScreenshotNeo reports page verdict and billing status in response headers and bills only clean shots.

FAQ

Can PHP capture a screenshot without JavaScript?

PHP can make HTTP requests, but a rendered screenshot requires a browser engine or a hosted service that runs one. Browsershot uses Puppeteer and headless Chrome or Chromium.

Should I use a full-page screenshot for every URL?

No. Use full page for documentation or archival views, and use a viewport, selector, or clip when you only need a component or preview.

Why does a screenshot differ between servers?

Browser version, installed fonts, viewport, device scale factor, timezone, network timing, and page state can all affect rendering. Keep those inputs consistent when comparing images.

Is a hosted API useful for queue workers?

Yes. It can move browser installation and process management out of the worker. Review credentials, URL privacy, request limits, and the provider’s billing behavior before production use.

What is the fastest path for a PHP application?

Use Browsershot when your deployment already supports Node.js and Chrome/Chromium. Use ScreenshotNeo when you want a single HTTP request without installing and operating that browser stack.

Sources