Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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

  1. Create the destination directory and grant the PhantomJS process write permission.
  2. Open the target page and call page.render() only after a successful load status.
  3. Use a supported extension whose format matches your intended response.
  4. Confirm the output file exists, has a non-zero size and is readable by the web server.
  5. Choose either a document-root URL or an authorization-aware PHP endpoint.
  6. Emit an <img> element with a meaningful alt value.
  7. Open the image URL directly and inspect its HTTP status, MIME type and response body.
  8. 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.