How to take a website screenshot with PHP
How to capture website screenshots from PHP using Puppeteer and Node.js -- from installing dependencies to a working PNG. Plus a faster way with an API.
I want to walk you through the full process of taking website screenshots from PHP today. Not theory, not a library overview -- the actual thing. Open a terminal, install dependencies, write code, run it, get a PNG file with a screenshot. The whole path from an empty folder to a working result.
Why would you even need this? Link previews for a directory site, automated OG images, screenshots for client reports, visual monitoring. Sooner or later a PHP project needs to turn a URL into an image.
The problem is that PHP can't render web pages on its own. There's no browser engine built in. So we'll need a helper -- headless Chrome through Puppeteer. It's a Node.js package, and yes, that means we need Node alongside PHP. Sounds like overkill, but you'll see it's not that bad.
Let's try it.
Installing Node.js
First things first -- we need Node.js. Puppeteer won't run without it.
If you're on macOS with Homebrew, one command:
brew install nodeOn Ubuntu it's a bit longer:
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejsQuick check that everything installed:
node -v
npm -vSee version numbers?

Good, moving on.
Creating the project and installing Puppeteer
Now we need a separate folder for our screenshot tool. I usually put it next to the PHP project, but it doesn't really matter where.
mkdir screenshot-tool
cd screenshot-tool
npm init -y
npm install puppeteerThis will take a minute. Puppeteer downloads a full Chromium binary during installation -- somewhere between 170 and 400 megabytes depending on your OS. Yeah, it's a chunky package. That's the price of getting a real browser you can control from code.
If you're on Ubuntu, Chrome might fail to launch the first time -- it needs system libraries that aren't there on a fresh install:
sudo apt-get install -y libnss3 libatk1.0-0 libatk-bridge2.0-0 \
libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 \
libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2On macOS you can skip this step -- everything works out of the box.
Writing the screenshot script
Now for the fun part. Create a file called screenshot.js inside the screenshot-tool folder:
const puppeteer = require('puppeteer');
const url = process.argv[2];
const output = process.argv[3] || 'screenshot.png';
if (!url) {
console.error('Usage: node screenshot.js <url> [output-file]');
process.exit(1);
}
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 30000,
});
await page.screenshot({ path: output, fullPage: false });
await browser.close();
console.log(output);
})();What this does: takes a URL from the command line arguments, launches headless Chrome, opens the page, waits for it to load, takes a screenshot, saves it to a file. Then closes the browser.
Let's test it right away:
node screenshot.js https://screenshotrun.com test.pngIf everything worked, you'll see a new test.png file in the folder:

Open test.png. If you see a screenshot of screenshotrun.com, the script works.

Try it with other sites too -- github.com, your own project, whatever you like.
Connecting it to PHP
The script works from the terminal. Now we need PHP to call it. The idea is simple: PHP runs our Node script through shell_exec() like any other console command, and picks up the resulting PNG from disk.
First, create a folder where screenshots will go. You can put it right inside your PHP project:
mkdir -p /path/to/your/php-project/screenshotsNow create a PHP file. I'll call it take-screenshot.php:
<?php
$url = 'https://screenshotrun.com';
$outputDir = __DIR__ . '/screenshots';
$filename = md5($url) . '.png';
$outputFile = $outputDir . '/' . $filename;
// Path to our Node script (use your own path)
$scriptPath = __DIR__ . '/../screenshot-tool/screenshot.js';
// Build the command
$command = sprintf(
'node %s %s %s 2>&1',
escapeshellarg($scriptPath),
escapeshellarg($url),
escapeshellarg($outputFile)
);
echo "Running: {$command}\n";
$output = shell_exec($command);
if (file_exists($outputFile)) {
echo "Done! Saved to: {$outputFile}\n";
echo "File size: " . round(filesize($outputFile) / 1024) . " KB\n";
} else {
echo "Something went wrong.\n";
echo "Output: {$output}\n";
}Note the 2>&1 at the end of the command -- it redirects stderr to stdout. Without it, if Node throws an error, PHP simply won't see it and you'll be left guessing why nothing works.
Run it:
php take-screenshot.phpIf everything went well, you should see something like this:

Go to the screenshots folder and open the file. If you see the screenshotrun.com homepage, you've got a working screenshot tool in PHP.

Adding options
The basic version works, but let's make it a bit more useful. We'll add viewport size and full-page capture support.
Updated screenshot.js:
const puppeteer = require('puppeteer');
const args = process.argv.slice(2);
const url = args[0];
const output = args[1] || 'screenshot.png';
const width = parseInt(args[2]) || 1280;
const height = parseInt(args[3]) || 800;
const fullPage = args[4] === 'true';
if (!url) {
console.error('Usage: node screenshot.js <url> [output] [width] [height] [fullPage]');
process.exit(1);
}
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const page = await browser.newPage();
await page.setViewport({ width, height });
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
await page.screenshot({ path: output, fullPage });
await browser.close();
console.log(JSON.stringify({ file: output, width, height, fullPage }));
})();And a PHP wrapper function so you don't copy-paste the same code every time:
<?php
function takeScreenshot(
string $url,
string $outputDir,
int $width = 1280,
int $height = 800,
bool $fullPage = false
): ?string {
$filename = md5($url . $width . $height . ($fullPage ? '1' : '0')) . '.png';
$outputFile = rtrim($outputDir, '/') . '/' . $filename;
$scriptPath = __DIR__ . '/../screenshot-tool/screenshot.js';
$command = sprintf(
'node %s %s %s %d %d %s 2>&1',
escapeshellarg($scriptPath),
escapeshellarg($url),
escapeshellarg($outputFile),
$width,
$height,
$fullPage ? 'true' : 'false'
);
shell_exec($command);
return file_exists($outputFile) ? $outputFile : null;
}
// Desktop screenshot
$file = takeScreenshot('https://screenshotrun.com', __DIR__ . '/screenshots');
echo $file ? "Desktop: {$file}\n" : "Failed\n";
// Mobile (iPhone-sized)
$file = takeScreenshot('https://screenshotrun.com', __DIR__ . '/screenshots', 375, 812);
echo $file ? "Mobile: {$file}\n" : "Failed\n";
// Full page with scrolling
$file = takeScreenshot('https://screenshotrun.com', __DIR__ . '/screenshots', 1280, 800, true);
echo $file ? "Full page: {$file}\n" : "Failed\n";
Run it:
php take-screenshot.php
Now you can take desktop, mobile, and full-page screenshots with one function call. Run it and compare the files -- the difference between desktop and mobile versions is pretty obvious.

There's a simpler way
We went through the whole thing: installed Node.js, downloaded Puppeteer with Chromium, wrote a JS script, hooked it up to PHP via shell_exec, built a wrapper with options. Does it work? It does.
But step back for a second. We pulled in an entire browser and a second language for one task: getting an image of a URL.
You can get the same result with a single HTTP request. No Node.js, no Chromium, no two scripts in two languages.
Here's what it looks like with the ScreenshotRun API:
<?php
$apiKey = 'YOUR_API_KEY'; // Get one free: screenshotrun.com/register
$ch = curl_init('https://screenshotrun.com/api/v1/screenshots');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://screenshotrun.com',
'format' => 'png',
'block_cookies' => true,
]),
CURLOPT_RETURNTRANSFER => true,
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
echo "Screenshot ID: " . $response['data']['id'];One file. One language. The only dependency is cURL, which PHP ships with by default.
The screenshot gets created on the API side -- the browser, fonts, everything is already handled there. A few seconds later you can download the image by its ID.
The free plan gives you 300 screenshots per month, no credit card. Enough to try it and see if it fits.
Vitalii Holben