marshmallow/nova-advanced-image
Advanced image field for Laravel Nova with cropping, previews, and configurable storage. Easily set dimensions, aspect ratios, and thumbnails through a clean API, giving your admin panel richer, more flexible image management.
Install and wire it up
composer require marshmallow/nova-advanced-image
php artisan vendor:publish --tag="nova-advanced-image-assets"
The vendor:publish step is not optional — this field ships front-end JS/CSS for the crop UI, and without publishing the assets the crop modal silently fails to bind (you'll get a plain file input with no crop trigger). If the package exposes a config tag, publish that too and read it before touching the field; storage disk and crop output settings usually live there.
Look first at
vendor/marshmallow/nova-advanced-image/src/ — the Field class is the source of truth for available fluent methods. Small packages like this (3 stars, zero dependents) often have sparse docs, so read the field class itself; it's usually under 200 lines.README.md on the GitHub repo for the exact method names (crop(), resize(), aspectRatio(), disk(), path() — verify, don't assume).resources/js/ in the package if you need to understand what the crop UI actually posts back (useful when debugging null crops).First use case — a hero image with a fixed aspect ratio
The canonical day-one scenario is a CMS hero/banner that must be cropped to a fixed ratio so marketing can't upload a 4000×300 portrait shot:
use Marshmallow\NovaAdvancedImage\AdvancedImage;
AdvancedImage::make('Hero Image', 'hero')
->disk('public')
->rules('required', 'image', 'max:5120')
->help('Crop to 16:9 — the front end renders a max of 1920px wide.'),
Register it exactly like a native Image field, run a normal nova:asset build (or rely on Nova's auto-discovery in newer versions), and save once. If a crop modal appears and the stored file reflects the crop, you're wired correctly.
Treat it as a drop-in for Nova\Fields\Image
The highest-leverage pattern: keep your existing migration and storage layout (hero string column, disk public) and swap only the field class in the Nova resource. The crop is applied before storage, so your model, accessor, and template code don't change. This makes adoption low-risk and reversible.
One field per ratio, multiple columns
Rather than trying to coax a single field into every context, define distinct fields for distinct crops:
AdvancedImage::make('Card Thumb', 'thumb')->crop(400, 400)->disk('public'),
AdvancedImage::make('Open Graph', 'og_image')->crop(1200, 630)->disk('public'),
AdvancedImage::make('Hero', 'hero')->crop(1920, 1080)->disk('public'),
This is clearer to editors (each has a labelled crop area), keeps the model predictable, and lets you apply per-column validation.
Pair with an index/thumbnail view
Show the field's underlying value on the index with ->thumbnail(fn () => $this->hero_url) so editors can spot broken or uncropped images without opening each record. If the package exposes a preview()/thumbnail() helper, use it — otherwise a small closure returning an <img> tag works.
Form composition
Because cropping is a form-time action, group image fields into a dedicated Image-titled Panel or Tab. This keeps the crop modal reachable without scrolling past unrelated fields and avoids the common UX complaint that the crop control is easy to miss.
Validation strategy
Layer validation in this order:
image, mimes:jpg,png,webp, max:5120) — reject non-images early.dimensions:min_width=1200,min_height=675 — prevent upscaling blur before the crop UI has a chance to.saving observer if you need to guarantee a non-null value.Don't rely on the crop UI alone to enforce minimum size; the API and any programmatic updates bypass it.
Publish assets in every environment, and re-publish on upgrade
This is the single most common failure. Deploy to staging without running vendor:publish --tag="nova-advanced-image-assets" (or nova:publish) and the crop modal won't appear. Add it to your deploy script alongside nova:assets. On package upgrade, re-publish; the JS is versioned with the PHP.
Old files are probably not cleaned up
Small image-field packages almost always store the new file and leave the old one on disk. If storage cost or GDPR matters, add a saved hook on the model to delete the previous path:
static::updated(function ($model) {
$old = $model->getOriginal('hero');
if ($old && $old !== $model->hero) {
Storage::disk('public')->delete($old);
}
});
Verify against the package source whether it already handles this before adding redundant logic.
Disk and URL consistency
Set ->disk('public') explicitly and keep it identical across all image fields in the resource. Mixing disks (default vs public vs S3) is the usual cause of "the crop saves but the image 404s." If you're on S3, confirm the package's crop output is written through the same Storage facade rather than move_uploaded_file-style direct writes — small packages sometimes hardcode a local path.
Crop UI is client-side; the server trusts what it gets
The crop coordinates come from the browser. If you expose this field on a public-facing form (unusual for Nova, but possible via custom tooling), treat the output as untrusted and re-validate dimensions server-side. Within Nova's authenticated panel this is a non-issue, but worth noting during code review.
Migrating from the native Image field
Existing records will render fine as long as the column and disk match — the crop UI simply shows the current image uncropped. But the first save on an old record may trigger a re-process/re-encode, changing the file path. Test on a copy of production data before rolling out, and log the old→new path mapping so you can redirect or restore if needed.
Extension points are thin — plan for a fork
With zero dependents and a recent-but-single-maintainer release, there is no ecosystem of extensions. If you need behaviour the field class doesn't expose (custom crop presets, responsive variants, per-locale images), budget for either a thin wrapper Field that extends the package class or a local fork. Read the source first — the resolveUsing/fillUsing hooks are usually public and enough to inject custom logic without forking.
Debugging blank crops
If the modal appears but the saved image isn't cropped, check in this order: (1) assets published and nova:assets rebuilt, (2) php artisan optimize:clear, (3) browser console for a crop-library init error, (4) the network payload — some versions post crop data only when the user drags, so a "save without touching the modal" leaves the original. Reproduce with an explicit drag before filing a bug.
Pin the version and watch release cadence
The last release is recent (Jan 2026) but stars and dependents are near zero. Pin an exact minor version in composer.json, and add a smoke test that uploads+crops a fixture image so a future package bump that changes the asset pipeline fails CI rather than production.
License and support expectations
MIT means you can fork freely — treat that as your support plan. For a low-traffic CMS it's a fine choice; for anything where a broken crop blocks editors, keep the wrapper thin enough that swapping back to vanilla Nova\Fields\Image (and a manual crop step) is a one-line change.
How can I help you explore Laravel packages today?