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

Statamic Cloudinary Laravel Package

tfd/statamic-cloudinary

Connects Statamic to Cloudinary so images and assets are stored, managed, and delivered through Cloudinary's CDN. Configure the asset driver and serve optimized, responsive media from your Statamic site with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up

composer require tfd/statamic-cloudinary
php artisan vendor:publish   # select the Cloudinary provider

Pin at least ^1.5.1 — v1.5.0 has a hard 500 on the wildcard tag syntax (see Gotchas).

The package config is published to config/statamic/cloudinary.php — not config/cloudinary.php. That distinction matters; the file in config/ at the package root is the default that gets merged under the statamic.cloudinary key at runtime. Edit the published one.

Add your Cloudinary credentials to .env (cloud name at minimum, plus API key/secret if you're doing authenticated uploads), then confirm the merge worked:

php artisan tinker --execute="dump(config('statamic.cloudinary'));"

Where to look first

Read these three files in order before writing any markup:

  1. src/Converter/CloudinaryConverter.php — the single source of truth for parameter translation and URL generation.
  2. src/Tags/Cloudinary.php — the Antlers tag, the wildcard() field shorthand, and both Glide fallback sites.
  3. src/Components/Image.php + resources/views/components/cloudinary-image.blade.php — the Blade surface.

Your first use case

Render a hero image through the tag in an Antlers template:

{{ cloudinary :asset="hero_image" transformations="w_1200,h_630,c_fill" fetch_format="auto" }}

There's also a field-handle shorthand — {{ cloudinary:hero_image }} — which routes through Tags\Cloudinary::wildcard() and is the form most day-to-day templates end up using for simple field rendering. Use the full tag when you need to pass transformations.

Or via the Blade component in a .blade.php view:

<x-cloudinary-image :asset="$hero" transformations="w_1200,h_630,c_fill" />

Both paths accept a Statamic asset ID, a Statamic asset URL, or a mapped external URL. Start with the tag — if Cloudinary isn't configured yet it will quietly hand off to Statamic Glide, so you can install it, leave credentials blank, and the site keeps rendering while you verify the integration. As of v1.5.1 that hold true for the wildcard() shorthand as well; in v1.5.0 the unconfigured-install path threw instead.


Implementation Patterns

Keep transformation definitions out of templates

The most common mistake in day-to-day use is scattering literal w_…,h_…,c_… strings across dozens of Antlers files. Instead, define named presets once — in a config array, a small helper, or an Antlers partial — and reference them:

<x-cloudinary-image :asset="$block->image" :transformations="$presets['card']" />

The converter accepts the transformation string as-is, so centralising it costs nothing and makes a design refresh a one-file change.

Prefer the Blade component for anything with markup semantics

The component exists to keep alt, width, height, and loading explicit alongside the generated src. The package conventions deliberately keep dimension/accessibility attributes visible in Blade rather than buried in PHP, so use <x-cloudinary-image /> for editorial content and reserve the raw tag for cases where you only need a URL (Open Graph tags, JSON-LD, CSS background-image values).

If you need the bare URL, resolve the converter rather than calling the tag:

use TFD\Cloudinary\Converter\CloudinaryConverter;

$url = app(CloudinaryConverter::class)->convert($asset, [
    'transformations' => 'w_1200,h_630,c_fill',
    'fetch_format' => 'auto',
]);

Prefer injecting CloudinaryInterface into your own services and binding it in a service provider — that keeps your code testable and lets you swap in a fake in Pest tests without touching the package.

Pick the tag form deliberately

Three entry points, three jobs:

  • {{ cloudinary:hero_image }} — field shorthand, no options. Fine for simple cases; this is the wildcard() path.
  • {{ cloudinary :asset="hero_image" transformations="…" }} — explicit; use when you need to shape delivery.
  • <x-cloudinary-image /> — anything with alt text, dimensions, or loading.

Mixing the shorthand and the explicit tag for the same field across templates is a common source of "why does this one look different" confusion. Pick per field type and be consistent.

Lean on the Glide fallback deliberately

src/Tags/Cloudinary.php falls back to Statamic Glide when Cloudinary is unavailable or the asset lookup fails (ItemNotFoundException is caught and logged). This is a feature, not a safety net you should duplicate. Don't wrap tag usage in your own try/catch — you'll just mask the log entries that tell you Cloudinary creds are missing in staging.

A good workflow: run staging without Cloudinary credentials so you exercise the fallback path, then flip credentials on in production. If both paths render acceptably, you've validated the integration. Note the two fallback sites live inside wildcard(), so validate both tag forms, not just the explicit one.

Use delivery_type and fetch_format for modern delivery

These are the two knobs that give you most of Cloudinary's value over plain Glide — fetch_format: auto lets Cloudinary negotiate WebP/AVIF from the Accept header, and delivery type lets you switch between upload and fetch for external sources. Set them as defaults in your preset helper rather than repeating them per call site.

Testing patterns

Tests use Pest with Tests\TestCase extending Statamic's AddonTestCase, so both Statamic and the addon service provider are booted. That means tag tests actually run the Statamic hooks — you can assert on rendered output end to end:

./vendor/bin/pest --filter=CloudinaryConverter
./vendor/bin/pest --filter=Tags/Cloudinary

When you add a new transformation parameter, add a unit test against CloudinaryConverter asserting the generated URL fragment. That class is small and pure enough that URL assertions are stable. v1.5.1 adds the first Tags\Cloudinary coverage; if you extend the tag, extend those tests — the wildcard path is where the recent crash lived and it was untested right up until this release.

Test the fallback explicitly by nulling credentials in the test env and asserting the Glide URL comes back rather than an exception.


Gotchas and Tips

Pin >=1.5.1; v1.5.0 500s on the wildcard tag. {{ cloudinary:field }} threw ArgumentCountError instead of falling back, at both fallback sites in Tags\Cloudinary::wildcard(). Two ways it bites: an install without Cloudinary credentials 500s on every wildcard tag, and a correctly configured install 500s the moment a referenced asset is missing or renamed. Both are exactly the cases the fallback was supposed to cover, which makes it a nasty regression — it turns a data problem into a white screen. If you're on 1.5.0, bump before debugging anything else. ArgumentCountError is not caught by the ItemNotFoundException handler, so you won't see a friendly log line, just a stack trace pointing at wildcard().

You published the config to the wrong place if URLs ignore your settings. The merge target is statamic.cloudinary. Editing config/cloudinary.php in the package root does nothing outside of local package development.

composer lint writes files. It runs php-cs-fixer fix, not a dry run. If you're doing a drive-by PR, use the check-only variant first:

php ./vendor/bin/php-cs-fixer fix --dry-run --diff -v --config .php-cs-fixer.php

The codebase is explicitly mixed — some older files are looser, newer ones use strict_types and typed properties. Match the surrounding file rather than reformatting it; a "tidy up types" commit on an old file will produce a large noisy diff and risks breaking callers.

Silent fallback can hide broken assets. When an asset ID doesn't resolve, the tag logs via Log::error(...) and renders through Glide. The page looks fine. Tail the log during integration:

tail -f storage/logs/laravel.log | grep -i cloudinary

If you see ItemNotFoundException entries in production, you have a data problem (deleted assets, stale IDs in entries), not a Cloudinary problem. If you see nothing in the log and images are still wrong, you're looking at a PHP-level error that bypassed the handler — see the v1.5.0 note above.

Parameter names are snake_case and mirror Cloudinary. fetch_format, delivery_type, transformations — not camelCase, not Cloudinary's dot-notation API form. When in doubt, read src/Tags/Cloudinary.php for the exact accepted names rather than guessing; there are only a handful of parameters and the file is short. This applies to the explicit tag only — the wildcard() shorthand takes no options at all, which is why there are two fallback call sites.

Don't rename the view namespace or component. The cloudinary view namespace and the <x-cloudinary-image /> alias are registered in src/ServiceProvider.php. Changing them is a breaking change for every consumer. If you need different markup, publish and override the view — but keep the alias.

Version pinning deserves attention. The package declares statamic ^5.73.11 || ^6.4, and the test harness runs on Laravel 12 with no CI matrix. That means the Statamic 5 / Laravel 10–11 combination is declared supported but untested by the maintainers. If you're on Statamic 5 with an older Laravel, pin the package version explicitly in composer.json and run composer test in your own app after any update — you are the CI for that combination.

Related, and worth knowing if you plan to contribute: until v1.5.1 the package's own dev toolchain was broken. require-dev pinned orchestra/testbench ^9 (Laravel 11), which is uninstallable because those releases are blocked by security advisories — so no test runner could be installed at all, and the Laravel 12 harness arrived with this release. If you're reading source on an older tag and composer test won't install, that's why; it isn't your environment.

Small package, small blast radius — read the source. With 7 stars and no dependents, there's no community knowledge base to fall back on. The good news is the whole surface is four classes. CloudinaryConverter is the one to understand deeply; everything else delegates to it. Bug reports and edge cases will land on your desk, so treat vendor/tfd/statamic-cloudinary/src/ as primary documentation alongside the README.

Extension point worth knowing: because parameter translation is centralised in CloudinaryConverter, adding support for a new Cloudinary transformation is usually a one-method change there plus a test. Resist the urge to pre-process the string in templates — you'll fork the translation logic and the fallback path will diverge from the Cloudinary path. If you touch wildcard(), change both fallback call sites and run the tag tests; that function's argument handling is the one place in the package with a recent crash history.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource