dmitrybubyakin/nova-medialibrary-field
A Laravel Nova field that integrates Spatie's Laravel Media Library, letting you upload, attach, and manage media files directly within Nova resources. Supports multiple collections, image conversions, and a clean, intuitive UI for faster admin workflows.
This package is a thin Nova adapter over spatie/laravel-medialibrary. Most behavior you will debug day-to-day is Spatie behavior, so learn the media library collections/conversions API first, then treat this field as the Nova UI for it.
Prerequisites
spatie/laravel-medialibrary installed and migratedSpatie\MediaLibrary\HasMediaCheck the package's composer.json against your Nova major before installing; Nova 4/5 compatibility is the first thing to verify.
Install
composer require dmitrybubyakin/nova-medialibrary-field
Install and migrate Spatie's media library if you have not already:
composer require spatie/laravel-medialibrary
php artisan vendor:publish \
--provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" \
--tag="medialibrary-migrations"
php artisan migrate
Publish the package config only if you need to change defaults; otherwise the package defaults are usually fine:
php artisan vendor:publish
# select the nova-medialibrary-field provider
Model setup
Register the media collections exactly as Spatie expects. The collection name is what the Nova field will target.
use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class Post extends Model implements HasMedia
{
use InteractsWithMedia;
public function registerMediaCollections(): void
{
$this->addMediaCollection('cover')->singleFile();
$this->addMediaCollection('gallery');
}
public function registerMediaConversions(?Media $media = null): void
{
$this->addMediaConversion('thumb')
->width(400)
->height(400)
->nonQueued();
}
}
Nova field
use DmitryBubyakin\NovaMedialibraryField\Fields\Medialibrary;
public function fields(NovaRequest $request)
{
return [
Medialibrary::make('Cover', 'cover')
->single()
->accept('image/*')
->preview()
->conversion('thumb'),
Medialibrary::make('Gallery', 'gallery')
->multiple()
->sortable()
->accept('image/*')
->preview()
->conversion('thumb')
->croppable(),
];
}
First use case
Start with a single cover image on an existing resource:
cover collection with singleFile().Medialibrary::make('Cover', 'cover')->single()->accept('image/*')->preview().<img src="{{ $post->getFirstMediaUrl('cover', 'thumb') }}" alt="">
Where to look first when something does not work:
config/media-library.php for disk, conversions, queueing, and URL generation.registerMediaCollections() / registerMediaConversions().media table row for the uploaded item: collection name, disk, custom_properties, generated_conversions.One field per collection
The second argument to Medialibrary::make() is the media collection name. Use separate fields for separate collections rather than trying to overload one collection with different semantics.
Medialibrary::make('Hero Image', 'hero')->single()->accept('image/*')->preview(),
Medialibrary::make('Downloads', 'downloads')->multiple()->accept('application/pdf'),
Single-file vs multi-file collections
Match the field mode to the Spatie collection definition:
// Collection
$this->addMediaCollection('avatar')->singleFile();
// Nova
Medialibrary::make('Avatar', 'avatar')->single()->accept('image/*')->preview(),
For galleries:
$this->addMediaCollection('gallery');
Medialibrary::make('Gallery', 'gallery')
->multiple()
->sortable()
->accept('image/*')
->preview()
->conversion('thumb'),
Use ->sortable() when editors need ordering. Spatie's order_column is the backing store.
Conversions and previews
Define conversions in Spatie, then point the field at the conversion used for display:
$this->addMediaConversion('thumb')
->width(300)
->height(300)
->sharpen(10);
Medialibrary::make('Images', 'images')
->multiple()
->preview()
->conversion('thumb'),
Keep conversion names stable. Renaming a conversion does not automatically regenerate existing media.
Cropping
Cropping is useful for hero images, avatars, and card thumbnails, but it depends on image conversions being configured correctly. Typical pattern:
Medialibrary::make('Cover', 'cover')
->single()
->accept('image/*')
->croppable()
->preview()
->conversion('thumb'),
If cropping is central to the editorial workflow, test it with the actual image driver used in production (gd vs imagick) and with the actual disk (public vs S3).
Custom metadata on media
If your version exposes per-media custom fields, use the field's fields() extension point to attach Nova fields to each media item (for example alt text or credits). This stores data in the media model's custom_properties JSON column.
Medialibrary::make('Gallery', 'gallery')
->multiple()
->fields(function () {
return [
\Laravel\Nova\Fields\Text::make('Alt text', 'alt'),
];
}),
Treat this as an extension point and verify the exact signature in your installed version. If custom fields do not persist, inspect the custom_properties column and the media model's fillable/casts.
Disk and storage strategy
Let Spatie own the disk configuration:
// config/media-library.php
'disk_name' => env('MEDIA_DISK', 'public'),
For local development:
php artisan storage:link
For S3 or private storage, confirm that the Nova preview can generate a usable URL. Private disks may require temporary URLs, and the field's preview may behave differently than on a public disk.
Frontend retrieval
The Nova field is only the admin UI. Your application still uses Spatie's API:
$coverUrl = $post->getFirstMediaUrl('cover', 'thumb');
$gallery = $post->getMedia('gallery');
foreach ($gallery as $media) {
echo $media->getUrl('thumb');
}
For responsive images or srcset, use Spatie's conversion URLs directly rather than relying on Nova previews.
Authorization and visibility
Since this is a standard Nova field, use Nova's visibility and authorization methods:
Medialibrary::make('Internal Assets', 'internal_assets')
->multiple()
->canSee(fn ($request) => $request->user()->isAdmin())
->canUpdate(fn ($request) => $request->user()->can('update', $this->resource)),
Do not rely on accept() for security. Treat it as browser/UI guidance only.
Queued conversions
For large images or slow storage, queue conversions:
// config/media-library.php
'queue_conversions_by_default' => true,
Then run a queue worker in every environment where uploads happen. If conversions are queued, the Nova preview may initially show the original until the conversion finishes.
Lifecycle hooks
Use Spatie media events for post-processing, optimization, or external sync:
MediaHasBeenAddedMediaHasBeenDeletedThis keeps the Nova field thin and moves business logic into listeners or observers.
Collection name mismatch is the most common failure
If uploads appear to succeed but nothing shows up, verify that:
Medialibrary::make() matches the collection name exactly.registerMediaCollections().InteractsWithMedia.media table row has the expected collection_name, model_type, and model_id.single() on the field is not the same as singleFile() on the collection
Use both for true single-file semantics:
$this->addMediaCollection('cover')->singleFile();
Medialibrary::make('Cover', 'cover')->single(),
Without singleFile(), replacements can accumulate media records even if the UI presents a single slot.
accept() is not validation
The accept attribute is a browser hint. Users can bypass it, and API requests definitely can. Add server-side validation for MIME type, size, dimensions, or media count if those constraints matter. Also check PHP limits:
upload_max_filesizepost_max_sizemax_file_uploadsLarge uploads often fail before Laravel or Nova sees the request.
Conversions must exist before the field references them
If ->conversion('thumb') points to a conversion that is not defined in registerMediaConversions(), previews will fall back to the original or appear broken. Define the conversion first, upload a fresh image, and confirm the generated_conversions JSON on the media row.
Changing conversions does not backfill old media
After changing widths, formats, or crop behavior, regenerate:
php artisan medialibrary:regenerate
Scope it to a model or IDs where possible. Regeneration can be slow and storage-heavy on large libraries.
Queued conversions can look like broken previews
If conversions are queued and the worker is not running, the original file exists but the thumb does not. Check:
generated_conversions on the media recordnonQueued()Cropping depends on the image driver and conversion setup
If the crop UI does not appear or crops do not apply:
Sorting requires the order column
->sortable() depends on Spatie's order_column. If you manually created or modified the media table, verify the column exists. If ordering behaves oddly, inspect order_column values directly.
Private disks and URLs
On S3 or other private disks, getUrl() may not be publicly accessible. Use temporary URLs where appropriate. Nova previews may work locally but fail in production if the disk visibility or URL signing is wrong.
Deleting and replacing media
Decide explicitly what happens when a user replaces an image:
Spatie provides APIs such as clearMediaCollection(), but replacement behavior depends on collection configuration and your own logic. Test replacement flows, not just first upload.
Custom fields need the right media model and casts
If per-media custom fields do not persist:
custom_properties on the media row.fields() callback is supported by your installed package version.Config caching
After changing package or media-library config:
php artisan config:clear
# or, if you deploy with cached config
php artisan config:cache
A stale config cache can make disk or queue changes appear to have no effect.
Nova and package version compatibility
This package has a modest ecosystem. Pin a tag that you have tested against your Nova major and Laravel version. Before upgrading Nova, check the package's composer.json constraints and recent releases. Because dependent count is low, edge cases may not be widely documented; reading the field source is often faster than searching for answers.
Test uploads with fakes
Use Laravel's storage fake and Spatie's testing helpers in feature tests:
Storage::fake('public');
$file = UploadedFile::fake()->image('cover.jpg');
// submit Nova request or call the media API directly
For Nova-specific tests, use Nova's testing helpers and assert the media collection and generated conversions rather than only asserting the HTTP response.
Keep business logic out of the field
The field is an admin adapter. Put image optimization, filename normalization, external CDN sync, and conversion policy into Spatie events, observers, or dedicated services. That keeps the Nova layer replaceable and makes the behavior testable outside the admin panel.
How can I help you explore Laravel packages today?