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

Mediable Laravel Package

tomshaw/mediable

Mediable is a lightweight Livewire media manager for Laravel that lets you upload, convert, and attach images, videos, and documents to blog posts, carousels, and product previews, with configurable file types, storage, and SEO-friendly names.

View on GitHub
Deep Wiki
Context7

Getting Started

Check the version gate before you do anything else. Mediable targets PHP 8.5, Laravel 13.20 and Livewire 4.x. On a Laravel 11/12 stack the current tag will not install — look at the release history (1.4.0 was the Laravel 13 / PHP 8.5 jump) and pin the last tag that matches your framework, or plan the upgrade first. Also confirm gd or imagick is compiled in, since uploads trigger WebP/AVIF conversion by default.

composer require tomshaw/mediable
php artisan mediable:install     # publishes config (and likely views/migration)
php artisan migrate              # creates the attachments table
php artisan storage:link

Then, in your layout, below your own Vite tags:

@vite(['resources/css/app.css', 'resources/js/app.js'])
@mediableStyles
@mediableScripts

and mount the component once:

<livewire:mediable fullScreen />

Set APP_URL correctly in .env — generated file URLs and image conversion depend on it — and php artisan config:clear after touching config/mediable.php.

Where to look first, in this order:

  1. config/mediable.php — three things actually matter day to day: validation.files.* (allowed mimes + max size in KB), disk, and folder (uploads by default).
  2. The attachments table after your first upload — that schema is what you'll be joining against, so read it before you design your own models around it.
  3. The two events: mediable.open (you fire it) and mediable.on (you listen; payload is $files).

First real use case — the 15-minute win: a blog post form with a "Insert media" button that opens the picker and drops the chosen file into a textarea.

// In your Livewire post-editor component
public function openPicker(): void
{
    $this->dispatch('mediable.open', id: 'body'); // injects into #body
}
<button type="button" wire:click="openPicker">Insert media</button>
<textarea id="body" wire:model="body"></textarea>

That single-input injection path is the lowest-friction integration and validates the whole toolchain (assets, storage link, disk, URLs) at once. Do this before building anything fancier.

Implementation Patterns

Mount once, open from anywhere. Put <livewire:mediable /> in the persistent layout, not on individual pages. Every page then has a picker available; if you mount it per-page, any mediable.open dispatched before the component hydrates is silently dropped.

Two integration modes, pick deliberately:

Injection mode (id: named parameter) — best for a single field: a rich-text body, a hero image input, a PDF link. It writes straight into the DOM node, so pair it with wire:model on that node and let Livewire pick the change up, or listen for the event and set the property yourself (see gotchas).

Event mode — best for anything structured: galleries, carousels, product previews, multiple attachments per record.

use Livewire\Attributes\On;

#[On('mediable.on')]
public function handleMedia(array $files): void
{
    // Inspect the payload shape once with Log::info($files) — it is not documented.
    $this->attachmentIds = array_merge($this->attachmentIds, array_column($files, 'id'));
}

The README shows on(['mediable.on' => fn ($files) => ...]); the #[On] attribute is the more conventional form and behaves the same. Use whichever your Livewire version prefers — the closure form is documented, so start there.

Persist references, not just paths. Store attachment IDs (a JSON column or a pivot table on your post/product model) and resolve URLs at render time through the configured disk. That survives a disk or folder change and lets you keep alt text, captions and ordering in your own domain, where it belongs:

$url = Storage::disk(config('mediable.disk'))->url($attachment->path);

Gallery / carousel pattern. Open with the event (no id:), let the user multi-select, then map the returned files into your own ordered collection. Store the order in your model — the media manager has no concept of "which image is first in this product's carousel".

Lean on the image pipeline instead of your own. WebP/AVIF variants are generated at upload time. Serve them with <picture> + <source type="image/avif">/image/webp, falling back to the original. Verify the generated filename convention on disk before you hardcode it — the README describes the behaviour, not the naming scheme.

Transform in the manager, not in code. Since 1.8.0 the picker exposes flip/scale/rotate/crop/filter/text through GD. For editors who need to correct a crop themselves, that removes a whole custom admin screen — but it does not replace a queue-based pipeline for thumbnails you control. Keep your own derived sizes separate.

Per-environment disks. public locally, s3 in production via MEDIABLE_DISK_DRIVER. Wrap that in config so nobody edits the published file per machine.

Housekeeping. 1.5.0 added garbage collection for hidden attachments. Run php artisan list | grep mediable to find the command, then schedule it daily — you will accumulate orphans, because files are uploaded and recorded the moment they're selected, not when the form is saved.

Keep it swappable. With 20 stars and zero dependents, treat this as a small dependency behind your own boundary: one Blade partial that renders <livewire:mediable /> and one service/trait that reads the config. If you later swap to a different browser, you change two files.

Gotchas and Tips

  • Event fired before mount = nothing happens. With wire:navigate, dispatch from a component that has just been swapped in and the listener may not exist yet. Safest: keep the mediable component in the layout (survives navigation), or dispatch from $nextTick/an Alpine x-on:click after the component is on the page.
  • Injected content can be wiped by a morph. DOM injection targets a node; a concurrent Livewire re-render can reset it. Capture via the mediable.on listener into a public property and bind that with wire:model if you see content vanish.
  • Livewire's own temporary-upload rules fire first. If livewire.temporary_file_upload.rules caps at 12MB, a 30MB file is rejected before Mediable's max:51200 ever applies, and the error looks like a Mediable validation failure. Raise both, plus PHP's upload_max_filesize and post_max_size.
  • max: is kilobytes. 10240 = 10MB, 51200 = 50MB. Easy to read as bytes and get a surprise.
  • The mimes whitelist is the security boundary. Adding svg because "the client wants logos" opens stored-XSS via embedded scripts — sanitize or rasterize, don't just append it. And for exotic containers (m4a, ogg, mov) mimes sometimes guesses wrong; fall back to mimetypes: if uploads are rejected with a valid file.
  • Clear config after every config edit. disk, folder, validation, and the WebP/AVIF quality settings all read through config; with config:cache in a deploy pipeline, an .env change alone does nothing. This is the #1 "why is it uploading to the wrong place".
  • AVIF will bite you on shared hosting. GD builds frequently ship without AVIF (sometimes without WebP). Check gd_info() / ImageMagick's format list; if unsupported, set MEDIABLE_CREATE_AVIF=false. Conversion is also CPU-bound and synchronous inside the request — large images can hit max_execution_time. Disable AVIF (or both conversions) until you can move it off the request path.
  • Asset directives order and duplicates. @mediableStyles/@mediableScripts must come after @vite and must not be included twice (partials included from two layouts is the usual culprit). If the modal renders but does nothing on click, that's almost always missing/failed JS — check the console before touching PHP.
  • Modal invisible = stacking context. A parent with overflow: hidden, transform, or a low z-index will bury it. fullScreen doesn't escape a clipped ancestor. Move the component up to <body> level if you see this.
  • APP_URL behind a proxy. If you're behind an HTTPS-terminating load balancer, generated URLs come out http:// and break. Set ASSET_URL/trusted proxies rather than hacking APP_URL.
  • S3 is not storage:link. Local symlinks are irrelevant there; you need object visibility (or temporary URLs) and a CDN if you care about latency. Test the full path on the real disk before shipping — the public-disk default hides permission problems entirely.
  • No polymorphic tie by default. The package stores uploads; it doesn't know your models. Deleting a post leaves its attachment rows. Own the cleanup — a deleting hook on your models plus the GC command on a schedule.
  • Debugging checklist when something's off: (1) php artisan config:clear, (2) storage:link exists and the file is actually on disk under folder, (3) disk visibility, (4) browser console, (5) Log::info($files) on the mediable.on payload — its shape is undocumented and may differ between releases.
  • Extension points: the published config, published Blade views (override the UI rather than forking), the Livewire event surface, and your own wrapper component. Use php artisan vendor:publish interactively to see the actual tags this version registers — don't assume a tag name from another package.
  • Pin the version. Single maintainer, tiny userbase, fast-moving requirements (PHP 8.5, Laravel 13). Use an exact constraint in composer.json and read the changelog before every bump, especially around 1.4.0's framework jump and 1.8.0's image-transformation additions.
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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager