daun/statamic-asset-thumbnails
A Statamic addon that generates and caches thumbnails for your assets, letting you serve properly sized, optimized images in templates and the control panel. Define thumbnail presets in config and reference them with a simple tag instead of hand-building image URLs.
The 1.2.1 release is mostly hardening, so getting started is unchanged — but the new failure semantics are worth wiring in from day one rather than retrofitting later.
composer require vendor/package
php artisan vendor:publish --tag=package-config
.env:
PACKAGE_API_KEY=
PACKAGE_THUMBNAIL_TIMEOUT=10
PACKAGE_THUMBNAIL_MAX_FILESIZE=5242880
new-ing it, so config and HTTP client bindings apply.Look here first: the config file (timeout/max filesize keys and the error-handling section), the package's exception classes (permanent vs transient are now distinguishable), and the service provider bindings if you need to swap the downloader or SDK client.
First use case: generate a thumbnail for an existing image record and persist it to a disk.
use Vendor\Package\Facades\Thumbnail;
$path = Thumbnail::for($image->source_url)
->width(400)
->store('thumbnails', 'public');
$image->update(['thumbnail_path' => $path]);
Then render with a guard for the missing-file case (see Gotchas):
@if ($image->thumbnail_path && Storage::disk('public')->exists($image->thumbnail_path))
<img src="{{ Storage::disk('public')->url($image->thumbnail_path) }}" alt="">
@else
<img src="{{ asset('images/placeholder.png') }}" alt="">
@endif
On-demand in a controller vs. pre-generated in a job. For low-traffic or admin-only surfaces, generate inline and let the new timeout cap the request. For anything user-facing at scale, push to a queued job so a slow upstream API doesn't hold a PHP-FPM worker:
final class GenerateThumbnail implements ShouldQueue
{
use Queueable;
public int $tries = 5;
public function __construct(public int $imageId) {}
public function handle(): void
{
$image = Image::findOrFail($this->imageId);
try {
$path = Thumbnail::for($image->source_url)
->width(400)
->store('thumbnails', 'public');
$image->update(['thumbnail_path' => $path, 'thumbnail_error' => null]);
} catch (PermanentApiException $e) {
// 4xx: retrying will never help. Record and stop.
$image->update(['thumbnail_error' => $e->getMessage()]);
$this->fail($e);
}
// TransientApiException / connection errors bubble up and the
// queue's backoff handles the retry.
}
public function backoff(): array
{
return [10, 30, 120, 300];
}
}
Branch on error class, not on status codes. 1.2.1's permanent/transient split is the main thing to adopt. Catch the permanent exception to stop retries and surface a user-facing state; let transient ones propagate to the queue or wrap them in your own retry loop for synchronous paths. A 429 and a 503 should not be treated the same as a 404.
Configure the guard rails per context. Short timeout for web requests, longer for jobs — override at call time where the package supports it, otherwise keep a conservative global default and rely on the queue for slow work. Set max filesize based on your largest legitimate source image, not the upstream maximum; it's your cheapest defence against a hostile or misconfigured CDN response filling a disk.
Wrap SDK exceptions at your boundary. The release expects you to handle SDK-level exceptions manually. Don't let vendor SDK exception classes leak into controllers — catch them in a small adapter/service and rethrow as your domain exceptions so callers only depend on two branches (retryable / not retryable).
Guard every read of a stored asset. Treat thumbnail_path in the database as a hint, not a guarantee. Centralise the check in a model accessor or a small ThumbnailUrl helper so the placeholder fallback isn't duplicated across Blade templates, API resources, and sitemap generation.
Test with fakes. Use Http::fake() for the HTTP layer and the package's own fake/mock hooks where available. Assert the two branches explicitly: one test where the fake returns a 4xx and you expect no retry, one where it returns a 5xx and you expect release()/backoff behaviour.
Content-Length or mid-stream, and keep PHP's memory_limit in mind for large jobs.--timeout and retry_after with headroom.catch (\Throwable) around the download call converts permanent misconfiguration (bad API key, revoked credentials) into silent retry storms. Log the exception class and, where available, the upstream response body — the SDK exception message alone is often unhelpful.408 and 429 are ambiguous; confirm how the package classifies them before assuming they're transient. If they're classified as permanent in this version, handle those explicitly in your adapter.thumbnail_path where the file is gone, rather than leaving dead references forever.public need the symlink (php artisan storage:link) on deploy; if you store to a private disk, generate temporary URLs instead of assuming url() works.config() at runtime for per-job settings. Prefer these over forking.Log::withContext call with the source URL and image ID) before reproducing failures; the timeout and filesize rejections are otherwise easy to mistake for empty responses. Laravel's HTTP client events and Telescope are useful if the package is built on the HTTP client and emits them.How can I help you explore Laravel packages today?