Laravel ImageTools: stop breaking CDN cache with stable, query-driven images

Every release gets stuck on images. CSS/JS already follow the rules—hashed filenames, long-lived CDN cache, predictable deploys. Images remain the manual risk: someone renames a file and emails 404, CDN caches “however it can,” S3/R2 isn’t consistent, and the same crops repeat across templates.

Laravel ImageTools: stop breaking CDN cache with stable, query-driven images

GitHub:isap-ou/laravel-imagetools

Why ImageTools matters

When a team asks for “just one more tweak” to a hero image, the blast radius can be bigger than the pixels suggest. A new filename slips into a template, an email goes out with the old URL, Open Graph cards show the wrong preview for days, and the CDN cache looks like a coin toss. The front end solved this years ago with hashed assets and immutable caching. Images deserve the same predictability—and that’s exactly what ImageTools brings to Laravel.

Instead of baking filenames by hand or relying on a build pipeline you don’t fully control, you describe what you need with a simple URL: the path to the source before the question mark, and a few clear options after it. ImageTools turns that intent into a real file with a deterministic name—think <name>--<hash>.<ext>—stores it on your chosen disk, and hands back a stable public URL. The hash changes only when the source or options change, so you can lean on long-lived CDN caching with confidence and stop chasing broken links after every release.

This isn’t a proxy route that transforms images on every HTTP request. It’s server-side generation you control from Blade or PHP: a one-time cost the first time a variant is needed, then instant responses afterwards. A tiny PHP manifest remembers the mapping between a canonicalized request and the stored file path, so the second, third, and thousandth call are as cheap as reading a string.

How it works in practice

You call ImageTools with a source plus options. The package canonicalizes the query (keys are sorted), computes a hash from the source and options, performs the transformation, writes the result to your storage disk, and returns a public URL. Because the filename includes the hash, immutable caching “just works”: change the image or its parameters and a new URL appears automatically; don’t change anything and the URL remains valid for months.

Query options you already know

Use a tiny, familiar vocabulary: w and h for dimensions (px), fit for geometry (from Spatie\Image\Enums\Fit), q for quality, and format for the target type (jpeg, png, gif, webp, avif). Under the hood, ImageTools uses spatie/image—Imagick is recommended; GD is available as a fallback. Canonicalization means ?h=630&w=1200 and ?w=1200&h=630 are the same request and yield the same URL.

Blade and PHP examples

<img
  src="{{ ImageTools::asset('public/images/hero.jpg?w=1200&h=630&fit=contain&format=webp&q=82') }}"
  width="1200"
  height="630"
  alt="Hero"
/>

You can also resolve the service from the container in plain PHP. Either way, the first call builds the variant and stores it; subsequent calls are just manifest lookups and string returns. By default, files land at something like image-tools/hero--a1b2c3d4e5.webp, and the manifest lives at bootstrap/cache/image-tools.php.

Storage & CDN

ImageTools works with any Laravel disk—local public, Amazon S3, Cloudflare R2, and others. Switching storage is a configuration change, not a template rewrite. Because filenames are hash-based, you can apply long TTLs and immutable caching policies at the CDN edge without worrying about stale content: the cache key is baked into the name.

Where it shines

Marketing pages and blogs keep OG and hero images stable across releases. Email templates stop breaking because URLs don’t drift with builds. Mixed stacks—Blade plus a SPA—can share the same server-side call. And migrations between storage backends become operational, not developmental: flip from public to S3/R2 in config and carry on.

Comparison: ImageTools vs other approaches

League Glide (HTTP transformer)

Glide exposes a public endpoint like /img/{path}?w=...&h=... that transforms images per request, often with signed URLs for safety. That’s great when you need a universal HTTP service. ImageTools takes a different path: it writes a physical file with a hash in its name and returns a static URL. No proxy route, no per-request work, and a natural fit for immutable CDN caching and static integrations like Blade views, emails, and OG images.

vite-imagetools (build-time)

vite-imagetools transforms assets during the Vite build (Sharp). If every image flows through JS/TS imports, that can be ideal. But if you render with Blade, send emails, or simply want server-side control in Laravel, ImageTools gives you a similar developer experience—query options, deterministic filenames—without tying your workflow to a frontend pipeline.

Best practices

Start with WebP and evaluate AVIF on a sample set for size and visual fidelity—some photos with smooth gradients need a closer look. Keep generation under your application’s control and whitelist valid source paths; scope write permissions to the target disk. With hash-based filenames in place, enable long-lived, immutable caching at your CDN and let cache-busting happen automatically when content truly changes.

Troubleshooting

  • Class not found: run composer dump-autoload.
  • Imagick/GD: install/enable Imagick (recommended) or fall back to GD.
  • Geometry:fit requires both w and h; pass numeric values only.
  • Public disk 404: ensure php artisan storage:link is configured.

Get in touch

Need help with Statamic?

Tell us your context and the outcome you want, and we’ll suggest the simplest next step.

By submitting, you agree that we’ll process your data to respond to your enquiry and, if applicable, to take pre-contract steps at your request (GDPR Art. 6(1)(b)) or for our legitimate interests (Art. 6(1)(f)). Please avoid sharing special-category data. See our Privacy Policy.
We reply within 1 business day.