optimistdigital/nova-media-field
A Laravel Nova field for attaching and managing media on your resources. Upload images or files, preview selections, and store them with your models, giving editors a simple media picker inside the Nova admin panel.
Install and publish
composer require optimistdigital/nova-media-field
php artisan vendor:publish --provider="OptimistDigital\NovaMediaField\NovaMediaFieldServiceProvider"
php artisan migrate
The package ships a service provider, a config file (thumbnails, disk, storage path) and a migration stub that creates the table holding media records. Publish and migrate before touching a resource — the field resolves its value against that table, so a missing migration surfaces as an empty field rather than an error.
Requirements to verify on the tag you install: Nova 2/3-era API, Laravel 8/9 range, GD or Imagick for thumbnail generation. Check composer.json in the repo for the exact constraints before adding it to a Nova 4/5 app — this package is archived (last release January 2023) and predates several breaking Nova field changes.
Register the field on a resource
use OptimistDigital\NovaMediaField\MediaField;
public function fields(Request $request)
{
return [
ID::make()->sortable(),
Text::make('Title'),
MediaField::make('Images')
->multiple() // single media if omitted
->rules('array') // see gotchas
->help('Upload new files or pick from existing media'),
];
}
Add the matching column to the resource's table (json on MySQL/Postgres preferred over text; cast it as array on the model).
Where to look first
README.md — the package is small enough that the README is close to complete documentation.src/MediaField.php — the entire public API surface (methods like multiple(), thumbnail sizing, disk selection). Read it end to end; it's one class.config/nova-media-field.php — thumbnail dimensions, storage disk, allowed mime types, max upload size.database/migrations/* — the exact schema of the media table. This is the most important file, because it defines the contract you'll write accessors, seeders and cleanup jobs against.resources/js/ — the Vue field component; only relevant if you need to fork or restyle.First use case
The canonical one: a Post resource with a cover image plus a gallery. Replace a bare File::make('Image') with MediaField::make('Images')->multiple() and you immediately get upload + browse + thumbnails + delete in one control, without writing a media model, upload controller or index preview yourself. A 30-minute spike — install, upload three images, then dd() the saved column and the media table rows — tells you everything you need to know about the storage contract before you build on it.
Contract-first development. Before writing frontend code, inspect what the field persists: typically the field column stores a reference (ID or path) or a JSON array of them, and a separate media table stores file metadata. Write a small read-only tinker session against a real upload, then build accessors around what you find rather than around what you assume.
Model accessors and casts. Keep the field's raw value in the database and expose clean URLs from the model:
protected $casts = ['images' => 'array'];
public function imageUrls(): array
{
return collect($this->images ?? [])
->map(fn ($item) => Media::find($item)?->url()) // adjust to actual API
->filter()
->values()
->all();
}
Centralising this in one place means a future migration to spatie/laravel-medialibrary only touches the model, not every Blade template.
Thumbnail strategy. Configure fixed thumbnail sizes in the config file and let the package generate them on upload, then reuse those sizes everywhere — index column, form preview, Blade card, OG image. Regenerating on the fly is the classic performance trap: an index listing 50 rows with dynamic resizing will hammer GD/Imagick.
Reuse across resources. The browsing feature is the real value: an editor picks an existing image instead of re-uploading it. Lean into this by standardising on one field name per collection (e.g. gallery, hero) so media stays discoverable across resources, and by treating the media table as a shared library rather than per-model storage.
Index and detail previews.
MediaField::make('Images')
->multiple()
->thumbnail(fn ($value) => /* first image URL */)
->onlyOnIndex(),
If the field doesn't expose a display callback in your version, use ->displayUsing(...) or a Stack of Image fields instead — keep the heavy media field on forms only.
Programmatic attachment. Seeders, importers and API endpoints should create media through the package's model rather than by guessing at the schema. Wrap it in a small MediaService so a future replacement of the package is a single-file change.
Deletion semantics. Decide early whether removing a media item from a field detaches it (record stays, reusable) or deletes the file. The field usually offers both via a config flag or per-field method. For a shared library, detach-only is almost always correct; file deletion is a separate admin action.
Frontend rendering.
@foreach ($post->imageUrls() as $url)
<img src="{{ $url }}" alt="" loading="lazy">
@endforeach
Cache the URL list per model in a MediaPresenter if you render it in loops.
composer.json, and plan to fork (repositories with a VCS entry) rather than wait for a patch. Budget a day for a Nova 4+ migration: field metadata, asset loading and Field base-class changes all moved.OptimistDigital\NovaMediaField\... in the newer tags, OptimistDigital\MediaField\... in some builds). If composer require succeeds but the class isn't found, grep vendor/optimistdigital/nova-media-field/src for the actual FQCN before debugging further.->rules('required') on a multiple field validates the array wrapper, not membership. Use 'required|array|min:1' and 'images.*' => 'integer|exists:media,id' (adjust table name) so a stale ID doesn't 500 the page.storage:link and disks. Thumbnails written to public need php artisan storage:link. On S3, ensure the disk's url and visibility are set (public or signed URLs) or you'll get 403s in the Nova index while uploads appear to succeed.php artisan config:clear after editing config/nova-media-field.php; a cached config is the most common "my new thumbnail size doesn't exist" cause.Post doesn't delete its media rows or files. Add an observer (deleted → detach/cleanup) or a scheduled media:prune command; otherwise the table and disk grow silently.json, not varchar(255) — multiple images blow past 255 characters fast, and MySQL will truncate rather than error in non-strict mode.photo.jpg can overwrite each other depending on the package's naming. Check the generated filename strategy and consider prefixing with a UUID in a custom upload handler if you accept user-supplied names.max_execution_time. Cap upload size in config, or move to a queued conversion if editors routinely upload camera originals.npm install && npm run prod) and publish assets. Stale compiled assets after a fork are a silent failure — the field renders but the upload button does nothing.spatie/laravel-medialibrary plus one of the maintained Nova media fields gives you conversions, responsive images, S3 support and PHP 8.3+/Nova 4+ compatibility. Use this package when the app is already on it and the cost of migration outweighs the maintenance risk — not for new builds.How can I help you explore Laravel packages today?