bbs-lab/nova-cloudinary-field
bbs-lab/nova-cloudinary-field provides a Laravel Nova field for seamless Cloudinary integration, letting you upload, preview, and manage images and media directly within Nova resources using Cloudinary's cloud storage and transformation capabilities.
Install the field into an existing Nova application:
composer require bbs-lab/nova-cloudinary-field
php artisan vendor:publish --tag=cloudinary-field-config # if a config tag is offered
Add your Cloudinary credentials to .env before touching any resource class — the field resolves credentials at render time, so a missing key shows up as a broken preview rather than an exception:
CLOUDINARY_URL=cloudinary://<api_key>:<api_secret>@<cloud_name>
# or the discrete form
CLOUDINARY_CLOUD_NAME=
CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=
Where to look first, in order:
config/cloudinary.php (or the package's own config) — confirms which env keys are actually read.src/*Field.php) — check the component() name so you know which Vue component renders in Nova, and read resolve()/fill() to see exactly what is stored in the database column.First use case: a cover_image column on a Post resource.
use BbsLab\NovaCloudinaryField\CloudinaryImage; // verify exact FQCN in src/
public function fields(NovaRequest $request)
{
return [
CloudinaryImage::make('Cover Image', 'cover_image')
->rules('nullable'),
];
}
Upload one image, save, then inspect the row with php artisan tinker — this single check tells you whether the column holds a Cloudinary public_id or a full delivery URL, which determines everything downstream.
Store the public ID, derive the URL. The field's main value is separating persistence from delivery. Persist the public_id (or whatever the field resolves) and build URLs at render time so you can change transformations without a data migration:
// app/Models/Post.php
public function coverUrl(int $width = 1200): ?string
{
if (! $this->cover_image) {
return null;
}
return cloudinary()->image($this->cover_image)
->resize(Resize::scale()->width($width))
->toUrl();
}
One field instance per column, transformations in the field config. Keep the Nova-side config focused on editor concerns (aspect ratio, crop mode, max upload size) and keep delivery concerns (responsive srcset, format negotiation f_auto,q_auto) in your application layer. Mixing them means every transformation tweak requires a Nova deploy.
Multiple images / galleries. For a JSON or media table setup, attach the field per-item inside a HasMany or use the field's multiple mode if available; validate array and constrain count with ->rules('array', 'max:8'). Don't rely on the field for cardinality enforcement — it's a UI affordance.
Index and detail views. Nova renders the field component in index/detail contexts too; if you have large galleries, add ->onlyOnForms() or ->exceptOnForms() deliberately so the index table doesn't attempt to load N thumbnails per row.
Integration with existing Cloudinary usage. If you already use cloudinary-labs/cloudinary-laravel, make sure both packages resolve credentials from the same source. Duplicated config (one package reading CLOUDINARY_URL, the other reading CLOUDINARY_CLOUD_NAME) is the most common source of "upload works in Nova but URLs 404" reports.
Testing. Never hit the real Cloudinary API in tests. Use Laravel's Http::fake() for the upload endpoint, or store fixture public IDs directly on factories and only assert on the generated URL shape. For Nova field tests, Nova::assertField() style assertions are enough — you rarely need to exercise the JS component.
Upgrading for Laravel 13. v2.2.0 is a compatibility release: it widens the illuminate/* / laravel/framework constraint to include Laravel 13. There is no field API change, no config change, and no migration. Treat it as a routine patch: composer update bbs-lab/nova-cloudinary-field and re-run your Nova field tests. Confirm your Nova major version supports Laravel 13 before bumping — Nova's own constraint is the real gate, not this package's.
config:cache and env changes. Any credential fix in .env requires php artisan config:cache (or config:clear) in cached environments. A stale config cache is the #1 cause of "credentials are set but uploads fail."deleted event to call cloudinary()->destroy($publicId) — and be careful with soft deletes, where you usually want to keep the asset until force-delete.public_id is silently dropped unless you diff in saving/saved. Capture the original value with $model->getOriginal('cover_image') inside an observer.q_auto) creates an entirely new derived asset and a cold cache. Standardise on a shared transformation helper rather than ad-hoc strings scattered across views.f_auto on non-image extensions. If the column can hold PDFs or videos (Nova's file fields often do), f_auto on a video public ID with the image endpoint returns a broken URL. Branch on resource type.resolveUsing / displayUsing callbacks are the cleanest place to customise what the Nova index displays (e.g. a low-res w_200 thumbnail) without forking the Vue component.--with-dependencies.How can I help you explore Laravel packages today?