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

Use Spatie Laravel Screenshot for the shortest Laravel implementation. Install the package with Composer, choose its local Browsershot driver or the hosted Cloudflare driver, set the viewport and wait conditions your page needs, and save the result through Laravel’s filesystem (including S3). The same API captures external URLs, rendered Blade HTML, full pages and JavaScript-driven content.

1. Install Laravel Screenshot and a rendering driver

From your Laravel project, install the package:

composer require spatie/laravel-screenshot

Browsershot is the default local driver. It runs Puppeteer with a headless Chrome or Chromium binary, so install its integration when you want the browser on your own VM or container:

composer require spatie/browsershot

Your deployment image must also contain compatible Node.js and Chrome/Chromium binaries. If you cannot install those dependencies, configure the package’s Cloudflare driver instead. Cloudflare Browser Rendering is called over HTTP and does not require local Node.js or a Chrome binary, but it does require Cloudflare credentials and account configuration.

Situation Driver Trade-off
VM or container where you control browser dependencies Browsershot Maximum local control, but you maintain Node.js, Chromium, fonts, sandbox settings and browser processes.
Serverless or locked-down hosting Cloudflare Browser Rendering No local browser installation; requires Cloudflare credentials and uses a remote rendering request.
End-to-end tests of your own Laravel UI Laravel Dusk Designed for browser automation and test screenshots rather than a general production capture service.

2. Capture a URL in a controller

Import the facade and save a reachable page. The package documentation lists defaults of a 1280×800 viewport, device scale factor 2, PNG output and a networkidle2 wait. Set values explicitly when consistency matters.

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

namespace AppHttpControllers;

use SpatieLaravelScreenshotFacadesScreenshot;

class ScreenshotController extends Controller
{
    public function store()
    {
        Screenshot::url('https://example.com')
            ->width(1440)
            ->height(900)
            ->save('screenshots/example.png');

        return response()->json([
            'path' => 'screenshots/example.png',
        ]);
    }
}

Register the action behind authentication if users can trigger it. The returned path is relative to the configured default filesystem disk. For production, use a deterministic name (for example, a report ID plus revision) so retries do not create uncontrolled duplicates.

3. Capture Blade-rendered HTML instead of a public URL

When the source is your own view, render it first and pass the resulting markup to Screenshot::html(). JavaScript embedded in that HTML runs during capture, allowing client-rendered charts and widgets to appear.

<?php

use SpatieLaravelScreenshotFacadesScreenshot;

$html = view('reports.preview', ['report' => $report])->render();

Screenshot::html($html)
    ->width(1200)
    ->height(800)
    ->save('reports/'.$report->id.'.png');

This approach avoids exposing an authenticated page to an outside renderer. For a page that must be rendered through a route, create a purpose-built, authorization-protected route and use a session or signed authorization mechanism designed for that route.

4. Full-page and JavaScript-rendered screenshots

A viewport screenshot stops at the configured height. Use fullPage() when the image should include the complete document:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Screenshot::url($url)
    ->fullPage()
    ->waitForSelector('#report-ready')
    ->save($path);

Dynamic pages need an explicit definition of “ready.” Useful controls include:

  • Wait for a selector: capture only after a known element such as #report-ready exists.
  • Wait for a delay: allow animations, charts or delayed API responses to finish.
  • Wait for JavaScript conditions: use a page-specific readiness condition when a selector is not reliable.
  • Full-page mode: let the browser lay out the entire document.
  • Viewport and device size: reproduce desktop, tablet or mobile layouts.
  • CSS and JavaScript injection: hide transient controls, force a print style, or finish a chart before capture.

Lazy-loaded images often appear only after scrolling. Full-page capture and an appropriate wait condition give the browser an opportunity to load them. If the target never reaches the selected condition, the capture can fail or run until its timeout. Set an explicit timeout, log the URL and rendering mode, and retry only idempotent captures.

5. Save screenshots to S3 or another Laravel disk

The package uses Laravel’s filesystem abstraction, so the application code can stay the same when storage moves from local disk to S3.

Screenshot::url($url)
    ->disk('s3', 'public')
    ->save('screenshots/'.$id.'.png');

The exact visibility is a security decision:

  • Public: suitable for genuinely public thumbnails or documentation images. Anyone with the object URL may retrieve it.
  • Private: use for reports, invoices and user data. Return a temporary or authorized download response rather than a permanent public URL.
  • Time-limited: generate a temporary filesystem URL when a reader needs access for a short period.

Store the disk name and object path in your database. Generate display URLs through Laravel’s filesystem API so local, S3 and future storage providers do not require separate controller logic. Add a retention job that removes old objects and corresponding records.

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

6. Queue slow or bursty captures

Launching Chromium or making a remote rendering request is too expensive for many normal HTTP requests. Queue the work and return a job or capture ID immediately.

Screenshot::url($url)
    ->disk('s3')
    ->saveQueued('screenshots/'.$id.'.png')
    ->then(function (string $path, ?string $diskName) use ($id) {
        // Persist the completed path and mark the capture ready.
    });

Make the job idempotent by recording a capture key or using a deterministic path. Limit worker concurrency: each local browser consumes CPU and memory, and too many simultaneous Chromium processes can cause crashes or swapping. Record start time, target URL, driver, viewport, wait condition and exception details. Configure queue retries with backoff, but do not blindly repeat a non-idempotent side effect.

7. Secure a screenshot endpoint

A route that accepts any URL can become a server-side request forgery (SSRF) primitive. Before allowing a user-supplied target:

  1. Require authentication and authorization.
  2. Prefer an allowlist of your own hosts or predefined route names.
  3. Reject localhost, loopback, link-local, private-network and metadata-service addresses after DNS resolution.
  4. Validate URL schemes and disallow unexpected ports.
  5. Do not forward user credentials in query strings.
  6. Apply request, queue and per-user rate limits.
  7. Redact URLs, cookies and authorization headers in logs.

For authenticated application content, render a protected Blade view directly where possible. Never send a user’s password or reusable session secret to a third-party screenshot service.

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

8. Test without launching a browser

Feature tests can replace real rendering with the package fake:

it('queues the report screenshot', function () {
    Screenshot::fake();

    $this->post(route('reports.screenshot', $report))
        ->assertOk();

    Screenshot::assertSaved(fn ($shot) =>
        $shot->url === route('reports.preview', $report)
    );
});

This verifies that your route requested the expected URL and path without requiring Chrome. Use Laravel Dusk when the test itself must exercise navigation, authentication, JavaScript interaction and visual checkpoints in a real browser.

9. Image format, viewport and reliability choices

  • Viewport: choose a width and height that match the consumer of the image; responsive breakpoints can change the entire layout.
  • Device scale: the documented default is 2. A lower scale reduces bytes; a higher scale improves detail at greater CPU and storage cost.
  • PNG: lossless and appropriate for text, charts and UI screenshots.
  • JPEG or WebP: useful when transfer size matters and small compression differences are acceptable.
  • Wait strategy: network-idle defaults are convenient, but analytics, streaming requests or long polls may prevent a stable idle point. A page-specific selector or condition is often more reliable.
  • Timeout: set one that reflects the slowest legitimate page, then expose failures in logs and monitoring rather than returning a blank file.

10. Troubleshooting common failures

Chrome or Node.js is missing

Browsershot cannot start its local browser. Install compatible Node.js and Chrome/Chromium in the deployment image, verify executable permissions and ensure the package’s browser path points to that binary. On locked-down hosting, switch to Cloudflare Browser Rendering.

The image is blank or missing charts

The capture happened before client rendering completed. Wait for a chart container or application-ready selector, add a bounded delay, and confirm that the browser can reach the page’s API endpoints.

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

The page times out

Check DNS, outbound firewall rules, redirects and resources that never finish. Replace a broad network-idle wait with a selector or JavaScript readiness condition, and set an explicit timeout.

Lazy images are absent

Use full-page capture, wait after the page has laid out, or inject JavaScript that triggers the page’s loading behavior. Confirm that image hosts permit requests from the rendering environment.

Private content returns a login page

The renderer has no authenticated session. Prefer Screenshot::html() for server-rendered content, or create a protected, short-lived route specifically for the capture. Do not put credentials in the target URL.

S3 uploads succeed but links fail

Check the selected disk, object key and visibility. Private objects need temporary or authorized responses; public objects require the bucket policy and URL configuration to permit reads.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

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

Workers crash under load

Reduce queue concurrency, enforce per-job timeouts, monitor memory, and recycle workers after repeated browser failures. Keep browser binaries and package versions aligned across deployment images.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo is a hosted website screenshot API and MCP server. It removes cookie banners, newsletter popups and chat widgets before capture; bot checks, blank pages, failed loads and cache hits are not billed, with the response identifying the page verdict and billing status. Its MCP tools—take_screenshot, get_page_info and capture_pdf—let Claude, Cursor and other MCP clients request captures. Free usage includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000.

Use one GET request from a Laravel job or controller. See the ScreenshotNeo API documentation for all options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

From Laravel-compatible PHP, the same request can be made with Guzzle or PHP’s HTTP client. The API also supports PNG, JPEG and WebP, full-page and element captures, device presets, custom CSS and JavaScript, waits, blocked resources, headers, cookies, authorization, geolocation, time zones, resizing, chosen cache TTLs, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call and a usage API. Those controls remove the need to package Chromium with your application while preserving page-specific rendering choices.

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

Create a free ScreenshotNeo account to start with 1,000 screenshots a month and no card.

Frequently Asked Questions

Can Laravel Screenshot capture a page that requires JavaScript?

Yes. Browsershot executes page JavaScript; use a selector, JavaScript condition or bounded delay so charts and other client-rendered elements finish before capture.

Should I use Laravel Dusk for production screenshots?

Dusk is intended for end-to-end browser tests. Laravel Screenshot is the more direct production capture API, while Dusk is appropriate when the screenshot is part of a browser test flow.

How do I keep generated screenshots from filling S3?

Store capture metadata, define a retention policy, and run scheduled cleanup for expired objects and database rows.

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

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.