Generate Website Preview Thumbnails in Laravel for an Indian Marketplace
Build a queued Laravel screenshot pipeline for marketplace listing thumbnails, with storage, failure handling, and SSRF protections.
To generate website preview thumbnails for marketplace listings in Laravel, validate the submitted destination, capture it with a headless browser through a queued job, validate the resulting image, and store it under an application-generated filename. Treat user-submitted URLs as an SSRF risk: browsers can follow redirects and fetch page subresources, so destination checks must cover the renderer’s effective network access.
Spatie Laravel Screenshot provides a Laravel-facing API for capturing URLs or supplied HTML. It supports Browsershot, which runs headless Chrome, and a Cloudflare Browser Rendering driver. The package currently lists PHP 8.4+ and Laravel 12+ requirements; check those against your app before adopting it. Its documented defaults include a 1280×800 viewport, device scale factor 2, PNG output, and waiting for network idle. These are package defaults, not universal thumbnail specifications.
1. Choose the capture approach
| Approach | Good fit | Requirements and tradeoffs |
|---|---|---|
| ScreenshotNeo API | Managed captures from one HTTP request, including a Laravel job | One call returns PNG, JPEG, WebP, or PDF. Cookie banners, newsletter popups, and chat widgets can be removed before capture. Only clean shots are billed; bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. |
| Spatie Laravel Screenshot with Browsershot | Captures controlled by your Laravel application | Requires Node.js and Chrome or Chromium on the server. Puppeteer controls the browser; account for browser processes and resource usage. |
| Spatie Laravel Screenshot with Cloudflare driver | Managed browser rendering while keeping browser binaries off the app server | Requires an enabled Cloudflare Browser Rendering account, API token, and account ID. No Node.js or Chrome binary is needed on the application server. |
| Laravel Dusk | Browser automation and screenshots in test workflows | Dusk documents browser testing, including responsive and element screenshots. For production listing assets, a dedicated screenshot pipeline is a closer fit. |
Compare deployment dependencies, browser control, queue and failure handling, destination security, output requirements, and cost under your own workload. The available research does not establish comparative price, throughput, or India-region latency for these choices.
2. Install and configure a Laravel capture backend
For a local Browsershot setup, install Spatie Laravel Screenshot using the package’s installation instructions and install its required Node.js and Chrome/Chromium dependencies on the worker host. For Cloudflare rendering, configure the account, token, and account ID as documented by the package. Use the package’s current compatibility requirements as a gate: PHP 8.4+ and Laravel 12+ are listed. The exact installation commands and driver configuration depend on the package version and your chosen driver; follow the linked documentation rather than copying a possibly stale command.
Define thumbnail requirements before tuning the browser: card aspect ratio, target pixel dimensions, image format, whether the whole page or a specific region matters, and how dynamic pages should be handled. Spatie’s documented default is 1280×800 at device scale factor 2 in PNG, waiting for network idle. For a listing card you may want a smaller viewport or a compressed JPEG instead. Test representative submitted sites because layouts, delayed assets, cookie overlays, and bot checks vary.
3. Validate submitted URLs and protect against SSRF
A screenshot renderer visits a destination chosen by a user. It can also follow redirects and load images, scripts, fonts, and other resources from that page. This makes the renderer a network boundary: a weak check could let an attacker probe internal services or cloud metadata endpoints. OWASP recommends an allowlist when possible and warns that accepting complete user-supplied URLs is difficult to validate safely.
- Prefer accepting a domain or seller website identifier and constructing the allowed URL yourself, rather than accepting an arbitrary complete URL.
- If arbitrary public websites are required, allow only HTTP and HTTPS, normalize and parse the host, reject credentials and unusual ports unless needed, and check resolved IPv4 and IPv6 addresses against local, private, loopback, link-local, and reserved ranges.
- Recheck redirect destinations and DNS resolution at the point of connection to reduce DNS rebinding and redirect bypasses. A validation check at form submission alone is insufficient if resolution changes before capture.
- Where possible, restrict renderer egress at the network layer so browser workers cannot connect to internal application services or sensitive address ranges.
- Set resource and time limits, and keep the capture workers separate from privileged application services where practical.
Do not treat a regular expression or superficial string check as an SSRF defense. URL parsers can disagree about unusual representations. Review the OWASP SSRF Prevention Cheat Sheet for the threat model and validation guidance.
4. Put capture work on a queue
Browser startup and page loading can take long enough to make an interactive listing request unpleasant. Spatie documents queued screenshot generation and warns that it can be slow, especially with Browsershot or Cloudflare. Save the listing first, enqueue capture, and show a neutral placeholder until the thumbnail is ready.
<?php
// In a controller after validating and saving the listing:
GenerateListingThumbnail::dispatch($listing->id);
return response()->json([
'listing_id' => $listing->id,
'thumbnail_status' => 'queued',
], 202);
The job below is an implementation outline: connect captureWithYourConfiguredDriver() and saveQueued() to the API and queued-storage method exposed by your installed Spatie package version. The package supports queued saving and selecting a storage disk; check the versioned docs for exact fluent method names and driver setup.
<?php
namespace App\Jobs;
use App\Models\Listing;
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;
use Throwable;
class GenerateListingThumbnail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 120;
public function __construct(public int $listingId) {}
public function handle(): void
{
$listing = Listing::findOrFail($this->listingId);
$listing->update(['thumbnail_status' => 'processing']);
try {
// Validate again in the worker, immediately before browser access.
$url = app(ListingUrlPolicy::class)->validatedPublicUrl($listing->website_url);
// Use Spatie Laravel Screenshot with your configured driver.
// Configure a fixed viewport, output format, and wait behavior.
// Save to a deterministic, application-generated path on your chosen disk.
$path = 'listing-thumbnails/' . $listing->getKey() . '.jpg';
app(ListingScreenshotCapture::class)->captureToStorage(
url: $url,
disk: 'public',
path: $path,
width: 1200,
height: 750,
format: 'jpeg',
quality: 82,
);
$listing->update([
'thumbnail_path' => $path,
'thumbnail_status' => 'ready',
'thumbnail_error' => null,
]);
} catch (Throwable $e) {
$listing->update([
'thumbnail_status' => 'failed',
'thumbnail_error' => 'capture_failed',
]);
throw $e; // Let the queue retry according to its configured policy.
}
}
}
ListingUrlPolicy and ListingScreenshotCapture above are application services you implement around your URL-security policy and installed package API; they are deliberately not presented as built-in Spatie classes. Keep retry count, timeout, backoff, and failure state aligned with observed behavior. Avoid logging secrets or full request headers.
5. Set thumbnail dimensions, output, and wait behavior
Browsershot documents controls for viewport size, JPEG output and quality, full-page screenshots, clipped or element capture, network-idle waiting, and blocking selected URLs or domains. Select settings to match the listing card rather than blindly keeping a full-page, high-resolution PNG:
- Viewport: use one consistent desktop viewport for predictable card framing. Pick dimensions that match the design’s aspect ratio.
- Format and quality: use JPEG for photographic pages when a smaller file matters; use PNG when crisp edges or transparency matter. Validate the actual encoded output.
- Full page versus viewport: full-page captures create tall assets and may include irrelevant page content. A viewport or a clipped region is often more suitable for a compact card.
- Wait condition: network idle can help with late resources, but pages with persistent requests may never become idle. Use a bounded wait and test pages with delayed rendering.
- Blocked resources: selectively blocking ads or trackers can reduce distractions, but avoid blocking scripts or styles needed to render the page. Network blocking is not an SSRF substitute.
- Dynamic content: set a stable viewport and consistent wait policy. A page may still personalize content or require a user interaction.
Test the chosen settings against seller sites that use responsive layouts, consent overlays, lazy loading, and bot protection. Provide a fallback rather than repeatedly retrying a page that cannot be rendered.
6. Validate and store the image safely
Use an application-generated filename or a listing ID, never a URL, listing title, or other user input as a path. OWASP’s Laravel guidance recommends validating file type and size and warns against allowing user input to dictate filenames or paths.
- Confirm the renderer returned an image and that the decoded type is one you allow.
- Enforce a maximum byte size and pixel dimensions before serving the file.
- Write to a private or public disk according to the marketplace’s display needs. Do not assume that a generated image should be publicly accessible by default.
- Set a retention policy for replaced thumbnails and failed job artifacts. The sources do not prescribe an India-specific retention period.
- Keep a status such as
queued,processing,ready, orfailedso the UI can show a placeholder and operators can identify stuck work.
7. Handle failure and refresh deliberately
External pages are variable. A destination may time out, return a bot challenge, render blank, reject automation, or change its layout. Show a neutral placeholder or a seller-provided image when capture fails. Offer a controlled retry path, and avoid retry loops that repeatedly spend browser resources on a permanently blocked site.
For refreshes, record when the last successful capture occurred and enqueue a new job only when the URL changes or your refresh policy says it is due. Make writes idempotent: a retry should safely replace the same listing thumbnail rather than creating unbounded orphan files. If a listing is deleted while a job is queued, make the job exit cleanly.
8. Performance, reliability, and cost
- Queue capacity: browser work consumes worker time and server resources. Measure queue wait, capture duration, memory, and failure rates on representative traffic before setting concurrency.
- Timeouts and retries: choose them from observed page behavior. Ensure the queue timeout, browser timeout, and web server limits agree; retries should be bounded and use backoff.
- Cache: avoid recapturing an unchanged URL on every listing view. Keep a refresh timestamp or content version and choose a retention policy that fits how often seller pages change.
- Storage: image dimensions and format determine storage and transfer costs. Generate thumbnails for the card’s actual display size and remove replaced assets under a deliberate policy.
- Backend cost: local browser rendering uses your worker infrastructure; a managed driver has its own account and service requirements. The research sources do not provide comparable rates, throughput, or latency, so measure your workload and review current provider pricing.
- Marketplace operation: capture failures should not prevent the listing itself from being created. Keep capture state separate from listing publication state unless your product explicitly requires a thumbnail.
Or skip the browser setup
ScreenshotNeo’s API documentation shows the request options. This one-call example requests a WebP image for a listing URL; keep the API key in server-side configuration and send the request from a Laravel queued job.
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}`);
With ScreenshotNeo, cookie banners, newsletter popups, and chat widgets are removed before the shot; bot checks, blank pages, timeouts, failed loads, and cache hits are never billed; and an MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000 shots. See ScreenshotNeo and the API docs. Sign up free for 1,000 screenshots a month, no card required.
cURL, Python, and Node.js request examples
These standalone calls use the documented API base and example target. In a Laravel application, run equivalent requests in a queued backend job, validate the target under your own SSRF policy, and validate the returned image before storing it. See the API documentation for parameters and response headers.
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
Troubleshooting
| Symptom | Likely cause | What to check |
|---|---|---|
| Worker cannot launch the browser | Node.js or Chrome/Chromium is absent or unavailable to the worker | Install the required binaries in the actual worker environment and confirm executable paths and permissions. Consider the Cloudflare driver if you want to avoid local browser binaries. |
| Package cannot install or methods are missing | PHP/Laravel version mismatch or documentation for another package release | Check the package’s current PHP 8.4+ and Laravel 12+ requirements, then use documentation matching the installed version. |
| Screenshot is blank or incomplete | Navigation failed, page content is delayed, or the chosen wait behavior does not match the site | Inspect capture status and response headers where available, try a bounded wait adjustment, and test with a representative page. Keep a fallback image. |
| Capture hangs or repeatedly times out | Persistent network activity, a slow origin, or an unreachable destination | Bound navigation and job timeouts, use a suitable wait condition, cap retries, and surface a failed state for manual or later retry. |
| Thumbnail shows a consent overlay or popup | The page presents a consent tool or popup that obscures the content | Use a capture configuration that can address the overlay, or ScreenshotNeo’s consent and popup removal. Test different seller platforms because behavior varies. |
| Unwanted internal or private destination is reachable | Destination validation did not cover redirects, DNS resolution, or browser subresources | Pause the affected capture path, enforce address-range checks and redirect validation, and restrict renderer egress at the network layer. |
| Image is too large or wrong format | High-resolution PNG or full-page output is being stored for a small card | Choose card-appropriate dimensions and JPEG/WebP where suitable; validate byte size and decoded type before storage. |
| Queue retries create duplicate or orphan files | Capture writes are not idempotent or replaced files are not cleaned up | Use a deterministic internal key, safely overwrite or version the asset, and clean up superseded files after a successful write. |
| ScreenshotNeo request returns an error | Invalid key, malformed URL, or a capture outcome that needs inspection | Check the API response and documented X-Page-Verdict and X-Billed headers. Keep secrets server-side and consult the API docs for supported parameters. |
FAQ
Should I use Laravel Dusk to make production thumbnails?
Dusk is documented for browser testing. A dedicated screenshot pipeline is a more direct fit for generating listing preview assets.
Can I capture supplied HTML instead of a public website?
Spatie Laravel Screenshot supports capturing a URL or supplied HTML. HTML capture can suit previews generated from marketplace-owned content, while URL capture needs the destination security controls described above.
Does the package’s 1280×800 default mean my card should use those dimensions?
No. It is a documented package default. Choose viewport and output dimensions based on the card design and the asset size you need to serve.
Are there special Indian marketplace rules established by these sources?
The research does not establish India-specific legal duties, permissions, copyright treatment, privacy requirements, or platform-policy rules for capturing and displaying third-party pages. Review applicable law and each platform’s terms for your particular use.


