Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Screenshot Laravel Package

directorytree/screenshot

Laravel package for capturing screenshots of web pages with a simple, driver-based API. Point it at a URL and get an image back, with support for popular screenshot services and headless browsers. Configurable, queue-friendly, and easy to drop into any Laravel app.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and publish config.

composer require directorytree/screenshot
php artisan vendor:publish --tag=screenshot-config   # verify the exact tag in the README

Read the published config/screenshot.php first — it is the whole package. Note the browser binary path, default viewport, output format, disk, and any node/npm paths. Everything else in the package is a thin fluent wrapper around those values.

2. Prove the runtime before you write app code.

The package is ~50 lines of Laravel sugar over a headless browser binary. The failure mode is never the facade — it's a missing Chromium, a Node version mismatch, or a sandbox flag the container doesn't allow. Run the package's own documented example from php artisan tinker before touching a controller:

use DirectoryTree\Screenshot\Facades\Screenshot;

Screenshot::url('https://example.com')->save('smoke/example.png');

If that works in tinker, it works. If it doesn't, fix the container, not the code.

3. First real use case: a queued screenshot job.

Screenshots are slow (hundreds of ms to tens of seconds) and memory-hungry. Never in a request/response cycle.

class CaptureScreenshot implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $timeout = 60;
    public int $tries = 3;
    public array $backoff = [10, 60, 300];

    public function __construct(public string $url, public string $path) {}

    public function handle(): void
    {
        Screenshot::url($this->url)
            ->save($this->path);
    }

    public function middleware(): array
    {
        return [(new WithoutOverlapping($this->url))->expireAfter(120)];
    }
}

Put it on a dedicated queue ($this->onQueue('screenshots')) with its own worker pool so a hung Chrome process can't starve your actual job pipeline.

4. Where to look next. README's API section for viewport/format/disk options, the published config for defaults, and your filesystem disks config — screenshots go through Laravel's Storage facade, so s3, public, and local all work the same way.

Implementation Patterns

Wrap it in your own interface on day one. This is the single highest-value pattern. Don't let Screenshot:: leak past one class:

interface ScreenshotCapturer
{
    public function capture(string $url, CaptureOptions $options): string; // returns storage path
}

final class PackageScreenshotCapturer implements ScreenshotCapturer
{
    public function capture(string $url, CaptureOptions $options): string
    {
        $path = $this->pathFor($url, $options);

        Screenshot::url($url)
            ->save($path);   // extend with viewport/format per the README API

        return $path;
    }
}

Swap to Browsershot, a sidecar, or ScreenshotOne later by writing a second implementation. Call sites never change. This costs you twenty minutes and buys you the exit plan.

Cache by URL + options hash. Screenshots are pure functions of (URL, viewport, format, auth state). Hash them, key the path, check Storage::exists() first:

$key = hash('xxh128', $url.'|'.$width.'x'.$height.'|'.$format);
$path = "screenshots/{$key}.{$format}";

Add a TTL if the source page is volatile. This is the difference between a workable feature and a queue that never drains.

Model the lifetime explicitly. Store the path on a domain record, not in your head:

// screenshotables: id, screenshotable_type, screenshotable_id, path, captured_at, checksum

This gives you cleanup targets (prune by captured_at), regeneration logic, and an audit trail. A bare Storage::put() with no record is a leak.

Fake it in tests. Never hit a real browser in your test suite. Bind a fake capturer in the container:

$this->app->bind(ScreenshotCapturer::class, FakeScreenshotCapturer::class);

Assert on the job dispatching and the path being persisted; assert on pixels only in a small, tagged integration suite that runs against the real runtime.

Guard the inputs. Before the job ever runs, validate:

  • Scheme is https (or http, but not file://, gopher://, ftp://).
  • Host is on an allowlist, or at minimum resolves to a public IP — not 127.0.0.1, not 169.254.169.254, not 10.0.0.0/8, not *.internal. This is SSRF and it's the real risk, not the package.
  • Redirects are capped or disabled; a redirect is an allowlist bypass.

Do the DNS resolution check inside the capturer, not at dispatch time — DNS can change between the two.

Use it for what it's good at. Link/social preview images, admin-dashboard snapshots, report thumbnails, internal visual QA, compliance archiving. Keep it away from billing-grade PDF rendering or anything with an uptime commitment — 34 stars and zero dependents is a fork-or-pin posture, not an SLA.

Gotchas and Tips

The package is not the risk — the browser is. Version drift between your local Chromium, CI, and production produces different images from identical code. Pin the browser version in every image and treat it as a build artifact. If your output looks subtly wrong in production only, this is almost always why.

Missing fonts are the classic silent-quality bug. Slim container images ship without fonts; your rendered pages come out with fallback glyphs or tofu boxes. Install a broad font package (fonts-liberation, fonts-noto, etc.) in the production image and add a visual smoke test that captures a known page with mixed text.

Chromium and sandbox flags. In Docker you'll typically need --no-sandbox (the package's underlying driver usually exposes this via config), and --disable-dev-shm-usage prevents crashes from the 64MB default /dev/shm. Both are container concerns, not package concerns — but the package won't tell you that.

Timeouts must be layered. Package/driver navigation timeout, job $timeout, WithoutOverlapping expiry, and the HTTP client timeout should all be set, in that ascending order. A missing navigation timeout means a slow page holds a worker for the full job timeout — and repeated slow pages become a queue outage.

Memory is the constraint, not CPU. Each capture spins a full browser context. Cap concurrency per worker (--max-jobs / process count) at a number you've actually measured. Unbounded parallel Chrome is the most common way teams take down their worker fleet.

Treat rendered output as untrusted. You're rendering arbitrary HTML — including HTML the page's author controls — into an image on your server. Keep captures on an isolated worker pool with no access to internal networks or credentials, and put a hard cap on response size / page load time. Don't run capture workers that also hold secret-bearing env vars if you can avoid it.

save() needs a disk. The path is resolved through Laravel's filesystem config. If save('foo/bar.png') writes somewhere unexpected, check which disk the package configures versus your app's default — they aren't always the same, and the directory must exist (or the package must create it; verify).

Reconcile the metadata before it enters your dependency register. The supplied assessment cites 14 stars while package metadata reports 33, and the last release is future-dated (2026-10-16). Both are unverified signals. Don't infer active maintenance from them. Check the actual repository commit history.

Pin the version and accept the fork posture. Zero dependents means no crowd to catch regressions. composer require directorytree/screenshot:^x.y with a committed lockfile, and keep the adapter thin enough that forking the package (it's MIT and small) is a day's work if upstream stalls.

Extension points, ranked by leverage: (1) your ScreenshotCapturer interface, (2) the published config, (3) the underlying browser driver's options (headers, cookies, proxy, full-page). If you find yourself needing something the package doesn't expose, check whether it's a driver option before reaching for a fork — and if you do need authenticated captures, that's a driver-level concern, usually handled with cookies/headers passed through config rather than anything the facade advertises.

Set a decision gate. Time-box the pilot — 30 days, one low-risk feature, instrumented for latency, failure rate, and storage growth. At the gate: promote, replace with a managed service, or build in-house. Without a gate, a prototype dependency quietly becomes permanent infrastructure, and this package's adoption profile doesn't support that.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo