How to Save a Webpage Screenshot to a Folder with PHP
Capture a rendered webpage in PHP with Playwright or Puppeteer, save it safely, handle full-page shots, permissions, failures, and automation.
Use a real browser engine, then pass an explicit filesystem path to its screenshot method. PHP does not render arbitrary webpages by itself. Playwright or Puppeteer loads the page, executes its JavaScript and CSS, and writes the resulting PNG, JPEG or WebP file.
$page->goto('https://example.com');
$page->screenshot(__DIR__ . '/screenshots/page.png');
The rest of this guide shows a complete Playwright PHP workflow, a Puppeteer alternative, full-page and element captures, safe filenames, permissions, waiting strategies, troubleshooting, and an API option when you do not want to operate a browser.
1. Prepare PHP and a browser runtime
You need PHP, Composer, a browser automation library, and the browser binaries required by that library. Install the package and browser runtime according to its installation instructions, then verify that the PHP process can launch Chromium or another supported browser.
Keep screenshots outside publicly served directories when they may contain private, authenticated or user-specific information. A storage directory such as storage/screenshots is safer than a web root.
2. Minimal Playwright PHP example
The following pattern creates a browser, opens a page, and saves a PNG under a path based on the script directory. The screenshot API accepts the destination path directly.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Playwright\Playwright;
$directory = __DIR__ . '/screenshots';
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
throw new RuntimeException("Unable to create {$directory}");
}
if (!is_writable($directory)) {
throw new RuntimeException("Directory is not writable: {$directory}");
}
$filename = $directory . '/example-' . date('Ymd-His') . '.png';
$playwright = Playwright::create();
$browser = $playwright->chromium()->launch();
$page = $browser->newPage([
'viewport' => ['width' => 1440, 'height' => 900],
]);
try {
$page->goto('https://example.com', [
'waitUntil' => 'networkidle',
'timeout' => 60_000,
]);
$page->screenshot($filename, [
'fullPage' => true,
'type' => 'png',
]);
if (!is_file($filename) || filesize($filename) === 0) {
throw new RuntimeException('The screenshot file was not created');
}
echo "Saved {$filename}" . PHP_EOL;
} finally {
$browser->close();
}
Use the current Playwright PHP package’s documented namespace and launch syntax if your package version differs. The important operations are stable: navigate, pass an absolute path to screenshot(), then close the browser.
3. Choose the screenshot scope
Viewport screenshot
A viewport capture records only what is visible in the configured browser window.
$page->screenshot(__DIR__ . '/screenshots/viewport.png', [
'fullPage' => false,
'type' => 'png',
]);
Full-page screenshot
Set fullPage to true to capture the complete scrollable document. Pages that lazy-load images may need scrolling or an explicit wait before capture.
$page->screenshot(__DIR__ . '/screenshots/full-page.png', [
'fullPage' => true,
]);
Element screenshot
Capture a focused region when a whole-page image is unnecessary. Use a locator or selector supported by your Playwright PHP version.
$card = $page->locator('.pricing-card');
$card->screenshot(__DIR__ . '/screenshots/pricing-card.png');
4. Control rendering before saving
Screenshot output depends on the browser environment. Set the viewport, color scheme, device scale factor, locale, timezone, fonts and animation state when repeatability matters.
$context = $browser->newContext([
'viewport' => ['width' => 1280, 'height' => 800],
'deviceScaleFactor' => 2,
'colorScheme' => 'dark',
'locale' => 'en-US',
'timezoneId' => 'UTC',
]);
$page = $context->newPage();
$page->goto('https://example.com', ['waitUntil' => 'domcontentloaded']);
$page->waitForSelector('main');
$page->waitForTimeout(500);
$page->screenshot(__DIR__ . '/screenshots/controlled.png', ['fullPage' => true]);
Prefer a selector that proves the required content exists over a fixed delay. A short delay is still useful for transitions, charts or fonts that finish after the DOM is ready. Disable animations with a style injection when visual consistency is more important than motion:
$page->addStyleTag(['content' => '* { animation: none !important; transition: none !important; }']);
5. Save files safely and predictably
- Build paths from
__DIR__or a configured absolute storage root; worker processes may start in a different current directory. - Create the directory recursively before launching capture.
- Use an explicit extension such as
.png; Puppeteer infers the image type from the extension. - Sanitize user-provided names and reject path separators to prevent path traversal.
- Include a UUID or job ID to avoid collisions when captures run concurrently.
- Apply retention rules so old images do not fill the disk.
function safeBasename(string $name): string
{
$name = preg_replace('/[^A-Za-z0-9._-]+/', '-', $name) ?: 'shot';
return trim($name, '.-') ?: 'shot';
}
$base = __DIR__ . '/screenshots';
$id = bin2hex(random_bytes(8));
$filename = $base . '/' . safeBasename('homepage') . '-' . $id . '.webp';
6. Puppeteer from a PHP application
Puppeteer is a Node.js browser automation library. A common PHP architecture starts a small Node worker or service and passes it a URL and output path. Puppeteer’s page.screenshot() accepts a path; if no path is supplied, it returns image data instead.
// capture.mjs
import puppeteer from 'puppeteer';
const url = process.argv[2];
const output = process.argv[3];
if (!url || !output) throw new Error('Usage: node capture.mjs URL OUTPUT');
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
await page.screenshot({ path: output, fullPage: true, type: 'png' });
} finally {
await browser.close();
}
$command = [
'node',
__DIR__ . '/capture.mjs',
'https://example.com',
__DIR__ . '/screenshots/example.png',
];
$escaped = array_map('escapeshellarg', $command);
exec(implode(' ', $escaped), $output, $status);
if ($status !== 0) {
throw new RuntimeException('Browser worker failed');
}
For a long-running application, a queue worker or persistent browser service avoids launching a new browser for every request. Keep untrusted URLs isolated and enforce outbound network policy.
7. Complete cURL, Python and Node.js alternatives
If PHP is only the application layer, these equivalent examples show the same destination-folder idea in other runtimes.
cURL
mkdir -p screenshots
curl -L 'https://example.com' -o screenshots/page.html
cURL downloads HTML; it does not render a webpage screenshot. Use it for source retrieval, not for a visual capture.
Python with Playwright
from pathlib import Path
from playwright.sync_api import sync_playwright
out = Path(__file__).parent / "screenshots" / "page.png"
out.parent.mkdir(parents=True, exist_ok=True)
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1440, "height": 900})
page.goto("https://example.com", wait_until="networkidle", timeout=60_000)
page.screenshot(path=str(out), full_page=True)
browser.close()
Node.js with Puppeteer
import { mkdir } from 'node:fs/promises';
import puppeteer from 'puppeteer';
await mkdir('./screenshots', { recursive: true });
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: './screenshots/page.png', fullPage: true });
} finally {
await browser.close();
}
8. Or skip the browser setup
ScreenshotNeo provides a website screenshot API. One GET request returns a PNG, JPEG, WebP or PDF, so PHP only needs to save the response body. See the ScreenshotNeo API documentation for options.
<?php
$url = 'https://stripe.com';
$out = __DIR__ . '/screenshots/stripe.webp';
if (!is_dir(dirname($out))) {
mkdir(dirname($out), 0775, true);
}
$query = http_build_query([
'access_key' => 'YOUR_API_KEY',
'url' => $url,
]);
$contents = file_get_contents("https://api.screenshotneo.com/v1/shot?{$query}");
if ($contents === false) {
throw new RuntimeException('Screenshot request failed');
}
file_put_contents($out, $contents);
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)
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 capture. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and whether the shot was billed. ScreenshotNeo also has an MCP server for Claude, Cursor and other MCP clients, with tools for screenshots, page information and PDFs. The free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Directory does not exist | The destination was never created. | Call mkdir(..., true) and check the return value. |
| Permission denied | The PHP-FPM, queue or container user cannot write there. | Change ownership or permissions for the storage directory and verify with is_writable(). |
| File saved in an unexpected place | A relative path is resolved from the worker’s current directory. | Build an absolute path with __DIR__ or configured storage. |
| Blank or partially rendered image | Capture happened before JavaScript, fonts or lazy images finished. | Wait for a meaningful selector, network idle, or a short controlled delay; scroll lazy content if needed. |
| Navigation timeout | The site is slow, blocked, or never becomes idle because of long polling. | Use domcontentloaded, wait for a specific selector, raise the timeout, and inspect the URL from the worker. |
| Cookie wall covers content | The site requires consent before showing the page. | Automate the consent action or use a capture service that handles known consent platforms. |
| Different pixels between runs | Viewport, fonts, timezone, animations, data or browser versions changed. | Pin those inputs and disable animations for visual comparisons. |
| Concurrent jobs overwrite files | All jobs use the same filename. | Add a UUID, database ID or timestamp with sufficient precision. |
| Huge full-page image | The document is extremely tall. | Capture a selector, split the page, or use a PDF for long documents. |
10. Performance, reliability and cost
- Launch overhead: Starting Chromium for every request is slow. Reuse a browser process or queue jobs when volume grows.
- Page weight: Ads, video and third-party scripts increase load time. Block unnecessary requests where your automation library supports it.
- Timeouts: Set an upper bound and record the URL, elapsed time and failure reason. Always close the browser in a
finallyblock. - Disk: PNG is lossless but large; JPEG or WebP can reduce storage. Add retention and monitor free space.
- Security: Treat URLs and page contents as untrusted. Restrict private network access and keep sensitive screenshots out of public paths.
- Visual tests: Compare screenshots only after controlling rendering inputs; otherwise differences may come from the machine rather than the page.
- Managed capture: ScreenshotNeo bills only clean shots; bot checks, blank pages, timeouts, failed loads and cache hits cost nothing. Its pricing is Free for 1,000 shots monthly, then $5 for 3,000, $15 for 15,000, $39 for 60,000, $99 for 250,000 and $249 for 1,000,000. Yearly billing gives two months free.
11. Checklist
- Install the browser runtime available to the PHP worker.
- Create an absolute destination directory.
- Verify ownership and write permission.
- Choose viewport, full-page or element scope.
- Wait for the content that must appear.
- Use a unique filename and explicit extension.
- Validate that the file exists and has nonzero size.
- Close the browser and remove old files.
12. FAQ
Can PHP take a screenshot without Chrome?
Not reliably for arbitrary modern webpages. A browser engine is needed to execute JavaScript and apply CSS; PHP coordinates that engine and stores the output.
Where does Puppeteer save the file?
At the path supplied in page.screenshot({ path: ... }). Relative paths are resolved from the Node process’s current working directory.
Should I use PNG or JPEG?
Use PNG for sharp text and lossless comparisons. Use JPEG or WebP when smaller files matter and slight compression is acceptable.
How do I capture only one component?
Locate the element and call its screenshot method, such as $page->locator('.card')->screenshot($path), instead of using a full-page capture.
Can a screenshot include authenticated content?
Yes, if the browser context is given the required session state, cookies or headers. Store resulting files securely and never expose credentials in logs or filenames.


