Laravel gives you multiple options for capturing webpage screenshots. This guide covers all three approaches — including when to use each — with working code you can drop into any Laravel 10/11 project.
Option 1: Browsershot (Puppeteer bridge)
Spatie's Browsershot wraps Puppeteer via Node.js. It's the most popular Laravel screenshot package.
Requirements
composer require spatie/browsershot
npm install puppeteerYou also need Node.js ≥ 18 on the server. In practice this means Puppeteer on every deploy — Chromium binaries, shared memory config, and regular crashes in constrained environments.
Basic screenshot
use Spatie\Browsershot\Browsershot;
Browsershot::url('https://example.com')
->windowSize(1280, 720)
->save('/tmp/screenshot.png');Full-page screenshot
Browsershot::url('https://docs.example.com')
->fullPage()
->waitUntilNetworkIdle()
->save('/tmp/docs-full.png');Store in S3
use Illuminate\Support\Facades\Storage;
$tmpPath = tempnam(sys_get_temp_dir(), 'browsershot_') . '.png';
Browsershot::url('https://example.com')
->windowSize(1280, 720)
->save($tmpPath);
Storage::disk('s3')->put('screenshots/example.png', file_get_contents($tmpPath));
unlink($tmpPath);Drawbacks: Requires Node.js + Puppeteer on every server. Crashes under load. Cold starts add 2-4 seconds. Chromium needs 200 MB RAM minimum.
Option 2: SnapSharp API (no server dependency)
The SnapSharp API runs Chromium for you — your Laravel app makes an HTTP request and gets back an image. No Node.js, no Chromium, no shared memory config.
Setup
No package required — Laravel's HTTP client handles it. Add your API key to .env:
SNAPSHARP_API_KEY=sk_live_your_key_hereBasic screenshot helper
// app/Services/ScreenshotService.php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class ScreenshotService
{
private string $apiKey;
private string $baseUrl = 'https://api.snapsharp.dev/v1';
public function __construct()
{
$this->apiKey = config('services.snapsharp.key');
}
public function capture(string $url, array $options = []): string
{
$params = array_merge([
'url' => $url,
'width' => 1280,
'height' => 720,
'format' => 'png',
], $options);
$response = Http::withToken($this->apiKey)
->timeout(30)
->get("{$this->baseUrl}/screenshot", $params);
$response->throw();
return $response->body();
}
public function captureToS3(string $url, string $path, array $options = []): string
{
$params = array_merge($options, [
'url' => $url,
'upload_to_s3' => true,
]);
$response = Http::withToken($this->apiKey)
->timeout(30)
->get("{$this->baseUrl}/screenshot", $params);
$response->throw();
return $response->json('s3_url');
}
}Register in config/services.php:
'snapsharp' => [
'key' => env('SNAPSHARP_API_KEY'),
],Full-page screenshot in dark mode
$image = app(ScreenshotService::class)->capture(
'https://yourapp.com/pricing',
[
'full_page' => true,
'dark_mode' => true,
'format' => 'webp',
'quality' => 85,
]
);
file_put_contents(storage_path('app/screenshots/pricing.webp'), $image);Screenshots behind authentication (with cookies)
$image = app(ScreenshotService::class)->capture(
'https://yourapp.com/dashboard',
[
'cookies' => json_encode([
[
'name' => 'laravel_session',
'value' => 'your_session_value',
'domain' => 'yourapp.com',
],
]),
'width' => 1440,
'height' => 900,
]
);Artisan command for bulk screenshots
// app/Console/Commands/CaptureScreenshots.php
namespace App\Console\Commands;
use App\Services\ScreenshotService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class CaptureScreenshots extends Command
{
protected $signature = 'screenshots:capture {--force}';
protected $description = 'Capture screenshots for all published pages';
public function handle(ScreenshotService $snapsharp): int
{
$pages = \App\Models\Page::where('published', true)->get();
$this->info("Capturing {$pages->count()} pages...");
$bar = $this->output->createProgressBar($pages->count());
$bar->start();
foreach ($pages as $page) {
if (!$this->option('force') && $page->screenshot_url) {
$bar->advance();
continue;
}
try {
$bytes = $snapsharp->capture($page->url, [
'width' => 1280,
'format' => 'webp',
'quality' => 80,
]);
$path = "screenshots/{$page->id}.webp";
Storage::disk('s3')->put($path, $bytes, 'public');
$page->update(['screenshot_url' => Storage::disk('s3')->url($path)]);
} catch (\Exception $e) {
$this->warn("\n Failed {$page->url}: {$e->getMessage()}");
}
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info('Done.');
return self::SUCCESS;
}
}php artisan screenshots:capture
php artisan screenshots:capture --force # Re-capture allQueue job for async screenshot capture
For non-blocking screenshot capture (e.g. triggered on model creation):
// app/Jobs/CapturePageScreenshot.php
namespace App\Jobs;
use App\Models\Page;
use App\Services\ScreenshotService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
class CapturePageScreenshot implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $backoff = 30;
public function __construct(public readonly Page $page) {}
public function handle(ScreenshotService $snapsharp): void
{
$bytes = $snapsharp->capture($this->page->url, [
'width' => 1280,
'format' => 'webp',
]);
$path = "screenshots/{$this->page->id}.webp";
Storage::disk('s3')->put($path, $bytes, 'public');
$this->page->update([
'screenshot_url' => Storage::disk('s3')->url($path),
'screenshot_at' => now(),
]);
}
}Dispatch after save:
// In Page model observer or controller
CapturePageScreenshot::dispatch($page)->delay(now()->addSeconds(5));Scheduled screenshots with Laravel Scheduler
Capture marketing pages every night:
// routes/console.php (Laravel 11) or app/Console/Kernel.php (Laravel 10)
Schedule::command('screenshots:capture')->dailyAt('03:00');Handling rate limits
SnapSharp returns HTTP 429 when your rate limit is exceeded. Wrap retries around it:
use Illuminate\Http\Client\RequestException;
function screenshotWithRetry(string $url, int $maxAttempts = 3): string
{
$attempt = 0;
$lastException = null;
while ($attempt < $maxAttempts) {
try {
return app(ScreenshotService::class)->capture($url);
} catch (RequestException $e) {
if ($e->response->status() === 429) {
$retryAfter = (int) ($e->response->header('Retry-After') ?: 60);
sleep($retryAfter);
} else {
throw $e;
}
}
$attempt++;
}
throw new \RuntimeException("Screenshot failed after {$maxAttempts} attempts");
}Option 3: Playwright (headless via proc_open)
You can shell out to Playwright from PHP, but this combines the worst of both worlds — the complexity of a headless browser with the fragility of proc_open. Avoid this in production. Use Browsershot (which wraps Node.js Playwright/Puppeteer properly) or the SnapSharp API instead.
Comparison
| Browsershot | SnapSharp API | |
|---|---|---|
| Setup | npm + Puppeteer install | API key only |
| Node.js required | Yes | No |
| Server memory | 200 MB+ (Chromium) | 0 (runs elsewhere) |
| Reliability | Chromium crashes under load | Managed, auto-scaled |
| Stealth mode | No | Yes (Starter+) |
| Full-page | Yes | Yes (Starter+) |
| Dark mode | Manual CSS injection | Native parameter |
| Custom cookies | Manual | Native parameter |
| Rate limiting | Your server's limits | Per-plan |
| Cost | Server time + memory | API pricing |
For most Laravel apps — especially if you don't already run Node.js — the SnapSharp API is significantly simpler to operate.
Related: PHP screenshot examples · Screenshot API docs · Automate screenshots in Python