Save the PhantomJS render in a directory your web server can serve, then reference its URL in an HTML <img> element. A server filesystem path such as /var/www/site/public/images/capture.png is not itself a browser URL. For private files, have a PHP endpoint validate the requested image, send the correct image MIME type, and stream the bytes with readfile().
How the file-to-browser path works
The workflow has three distinct namespaces:
- PhantomJS output path: where the renderer writes the image on the server.
- Web-server mapping: which filesystem directories correspond to public URL paths.
- Browser URL: the address used in
src, such as/images/capture.png.
Keep these separate. If your document root is /var/www/site/public, writing to /var/www/site/public/images/capture.png normally makes the image available at https://your-domain.example/images/capture.png. The absolute filesystem path must not be placed in the HTML attribute.
PhantomJS is legacy software, and its official documentation is also legacy documentation. Check that its runtime, Qt build, TLS behavior and JavaScript support are compatible with your current operating system before adopting this workflow in a new production system.
Generate the image with PhantomJS
The official page.render API saves the rendered page to the filename you provide and normally selects the format from its extension. PNG and JPEG are practical choices for webpage images. The API documents PNG, JPEG, BMP, PPM and PDF output; GIF support depends on the Qt build.
#1 Best Overall
Save this as capture.js:
var page = require('webpage').create();
var output = '/var/www/site/public/images/capture.png';
page.open('https://example.com/', function (status) {
if (status === 'success') {
page.render(output);
console.log('Saved ' + output);
} else {
console.error('Page failed to load: ' + status);
}
phantom.exit();
});
Run it with the PhantomJS executable available on your server:
phantomjs capture.js
The load-status check matters: rendering after a failed navigation can create a missing, incomplete or misleading result. In a real job, also log the target URL, output path and exit status. Ensure the destination directory already exists and that the account running PhantomJS can write there.
PhantomJS documentation: render API and screen-capture guide.
Option 1: display a public image with a static URL
Use static delivery when the screenshot is not sensitive and ordinary web-server caching is desirable. Place the output below the document root, then emit the URL path:
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Fix the driver behind crashes, sound loss and screen glitches3Repair Windows errors before they cause bigger problems<img src="/images/capture.png" alt="Screenshot of the rendered page">
For a dynamically named file, store the generated filename with the relevant record and escape it when producing HTML:
<?php
$filename = 'capture-20260929.png';
$url = '/images/' . rawurlencode($filename);
?>
<img src="<?= htmlspecialchars($url, ENT_QUOTES, 'UTF-8') ?>"
alt="Generated webpage screenshot">
Use predictable names only when overwriting is intentional. Otherwise generate unique names and remove old files with a retention policy. Never expose an unvalidated filesystem path supplied by a request parameter.
Rank #2
Option 2: stream a private image through PHP
Keep captures outside the public document root when they require authorization, have a sensitive URL, or need dynamic access checks. The browser still uses a URL, but that URL points to PHP rather than directly to the file.
<?php
// image.php
$file = __DIR__ . '/private-images/capture.png';
if (!is_file($file) || !is_readable($file)) {
http_response_code(404);
exit;
}
header('Content-Type: image/png');
header('Content-Length: ' . filesize($file));
readfile($file);
exit;
Reference the endpoint normally:
<img src="/image.php" alt="Private generated screenshot">
PHP’s header() function must run before any output. A blank line outside PHP tags, warning, debug statement or included template can corrupt the response or prevent headers from being sent. readfile() writes the file bytes to the response; it does not authorize access by itself.
For an identifier-based endpoint, map an allow-listed identifier to a known directory instead of concatenating arbitrary query-string input:
<?php
$id = $_GET['id'] ?? '';
$files = [
'home' => __DIR__ . '/private-images/home.png',
'invoice-preview' => __DIR__ . '/private-images/invoice-preview.png'
];
if (!isset($files[$id])) {
http_response_code(404);
exit;
}
if (!is_readable($files[$id])) {
http_response_code(404);
exit;
}
header('Content-Type: image/png');
header('Content-Length: ' . filesize($files[$id]));
readfile($files[$id]);
exit;
Perform the user’s authentication and authorization checks before selecting the file. If formats vary, derive the MIME type from your trusted metadata and keep the extension, stored content and header consistent.
References: PHP header() and PHP readfile().
When PHP receives image bytes from another process
If a renderer or service returns binary content to PHP, write it in binary-safe mode and check the result. file_put_contents() creates a missing file and overwrites an existing file by default.
<?php
$bytes = $rendererResponseBody;
$path = __DIR__ . '/public/images/capture.png';
$written = file_put_contents($path, $bytes);
if ($written === false || $written !== strlen($bytes)) {
throw new RuntimeException('Image write failed');
}
Do not treat a non-false return value as proof that the complete workflow succeeded: check the byte count, then verify that the resulting file exists and is readable by the web server. See the PHP file_put_contents() documentation.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Choose static serving or a PHP endpoint
| Requirement | Recommended pattern | Reason |
|---|---|---|
| Public, non-sensitive screenshots | Static URL | Least code and straightforward browser and cache behavior. |
| Authentication or per-user authorization | PHP endpoint | Checks access before streaming bytes. |
| Files outside the document root | PHP endpoint | Keeps storage inaccessible as a direct URL. |
| Stable, high-volume assets | Static URL, optionally behind a web-server cache | Avoids invoking PHP for every image request. |
| Dynamic selection by record or token | PHP endpoint with an allow-list | Separates public identifiers from filesystem paths. |
End-to-end implementation checklist
- Create the destination directory and grant the PhantomJS process write permission.
- Open the target page and call
page.render()only after a successful load status. - Use a supported extension whose format matches your intended response.
- Confirm the output file exists, has a non-zero size and is readable by the web server.
- Choose either a document-root URL or an authorization-aware PHP endpoint.
- Emit an
<img>element with a meaningfulaltvalue. - Open the image URL directly and inspect its HTTP status, MIME type and response body.
- Set cache and retention behavior appropriate to whether the capture is public or private.
Troubleshooting common failures
Broken-image icon
Open the exact src URL in a new tab. A 404 usually means the URL-to-document-root mapping or filename is wrong. A 403 indicates permissions or server rules. Confirm the generated file exists at the expected filesystem path.
Works from the command line, not in the webpage
Compare PhantomJS’s output directory with the web server’s document root. A path valid to the shell is not automatically reachable by HTTP. Check virtual-host configuration and URL prefixes.
PHP endpoint downloads a file or shows garbled output
Send the matching Content-Type before the bytes, remove all debug output and warnings, and stop execution after readfile(). PNG requires image/png; JPEG requires image/jpeg.
Image is absent after generation
Log PhantomJS’s load status, output filename and process exit code. Check directory existence and write permissions. If PHP wrote the bytes, test the file_put_contents() return value and byte count.
Wrong format
Align the PhantomJS filename extension, actual file format and endpoint MIME type. The render API normally infers format from the extension; do not call a PNG file a JPEG merely by changing its URL.
Private endpoint exposes files
Do not accept ../-capable paths or arbitrary filenames from a query string. Authenticate the request and resolve a validated ID through an allow-list or database record.
Rank #4
Blank or incomplete capture
Verify that navigation returned success, and account for pages that depend on delayed scripts or resources. PhantomJS’s age can also cause modern TLS, JavaScript or layout incompatibilities; test the exact target and runtime rather than assuming current browser behavior.
Or skip the browser setup
ScreenshotNeo provides a one-request website screenshot API and MCP server. It removes cookie banners, newsletter popups and chat widgets before capture; bot checks, blank pages and failed loads are not billed; and its MCP tools let Claude, Cursor or another MCP client call take_screenshot, get_page_info and capture_pdf. The free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000 shots.
Recommended Free Tools
For a PHP application, call the API from your server and save the response body as an image:
<?php
$url = 'https://api.screenshotneo.com/v1/shot';
$query = http_build_query([
'access_key' => 'YOUR_API_KEY',
'url' => 'https://stripe.com'
]);
$context = stream_context_create(['http' => ['timeout' => 90]]);
$bytes = file_get_contents($url . '?' . $query, false, $context);
if ($bytes === false) {
throw new RuntimeException('Screenshot request failed');
}
file_put_contents(__DIR__ . '/public/images/shot.webp', $bytes);
See the ScreenshotNeo documentation for request options. The equivalent command-line, Python and Node.js calls are:
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}`);
After saving the returned bytes, display them with the same static-URL or PHP-endpoint patterns described above. Create a free account at ScreenshotNeo sign-up.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.FAQ
Can an HTML image tag use a PhantomJS filesystem path?
No. Browsers request URLs. Map the file into a public URL or stream it through an HTTP endpoint.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Should I use PNG or JPEG?
Use PNG when sharp text or transparency matters; use JPEG when photographic content and smaller files matter. Keep the extension and response MIME type aligned.
Does readfile() protect a private image?
No. Your endpoint must authenticate and authorize the request before selecting and reading the file.
Why must headers precede image bytes?
HTTP headers, including Content-Type, are sent before the response body. PHP cannot reliably change them after output has begun.
Frequently Asked Questions
Can an HTML image tag use a PhantomJS filesystem path?
No. Browsers request URLs. Map the file into a public URL or stream it through an HTTP endpoint.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallShould I use PNG or JPEG?
Use PNG when sharp text or transparency matters; use JPEG when photographic content and smaller files matter. Keep the extension and response MIME type aligned.
Does readfile() protect a private image?
No. Your endpoint must authenticate and authorize the request before selecting and reading the file.
Why must headers precede image bytes?
HTTP headers, including Content-Type, are sent before the response body. PHP cannot reliably change them after output has begun.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

