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

Filament Curator Laravel Package

awcodes/filament-curator

Filament Curator is a media picker and manager plugin for Filament Admin, offering a full media library UI with Glide-signed image URLs and a one-command installer. Supports Filament 3, 4, and 5. Note that it does not work with Spatie Media Library.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up in four commands/edits:

composer require awcodes/filament-curator
php artisan curator:install   # publishes config + migrations, runs curator:token
php artisan migrate

Then:

  1. Add the token to every environment. curator:install writes CURATOR_GLIDE_TOKEN into your .env only. Add the empty key to .env.example and set a value in CI/staging/production:

    CURATOR_GLIDE_TOKEN=
    

    Each environment can (and should) have its own value — URLs are signed and validated by the same app instance that rendered them.

  2. Register the plugin on the panel so the Media resource appears in navigation:

    use Awcodes\Curator\CuratorPlugin;
    use Filament\Support\Icons\Heroicon;
    
    ->plugins([
        CuratorPlugin::make()
            ->label('Media')
            ->navigationGroup('Content')
            ->navigationIcon(Heroicon::OutlinedPhoto),
    ])
    
  3. Add the plugin CSS to your custom theme (Filament panels require a custom theme first — see the Filament styling docs):

    @import '../../../../vendor/awcodes/filament-curator/resources/css/plugin.css';
    @source '../../../../vendor/awcodes/filament-curator/resources/**/*.blade.php';
    

Where to look first: config/curator.php (disk, directory, path generator, defaults) and Awcodes\Curator\Models\Media — that's the model everything else points at.

First use case — attach an image to a resource. Add the picker to the form and the column to the table:

use Awcodes\Curator\Components\Forms\CuratorPicker;
use Awcodes\Curator\Components\Tables\CuratorColumn;

CuratorPicker::make('featured_image_id')
    ->relationship('featured_image', 'id'),

CuratorColumn::make('featured_image')->size(40),
// Model
public function featuredImage(): BelongsTo
{
    return $this->belongsTo(Media::class, 'featured_image_id', 'id');
}

That single flow — upload once into the library, reuse across any resource — is the whole value proposition. Once it's working, everything else (curations, glider output, path generators) is refinement.

Implementation Patterns

Single vs. multiple media. Single is a belongsTo on an FK column. Multiple is a belongsToMany pivot — and you must call ->multiple() on the picker or the relationship won't behave:

CuratorPicker::make('product_picture_ids')
    ->multiple()
    ->relationship('product_pictures', 'id')
    ->orderColumn('order'),
public function productPictures(): BelongsToMany
{
    return $this->belongsToMany(Media::class, 'media_post', 'post_id', 'media_id')
        ->withPivot('order')
        ->orderBy('order');
}

orderColumn('order') is only needed if your pivot column isn't named the default — renaming it incorrectly is a silent no-op on drag-reorder.

Always eager-load relationships feeding a CuratorColumn. The column resolves media per row; without eager loading you get a query per row (and per image with multiple()):

->modifyQueryUsing(fn (Builder $query) => $query->with('featured_image', 'product_pictures'))

Render in Blade with <x-curator-glider>. This is the day-to-day output path — it generates signed Glide URLs and handles responsive srcset:

<x-curator-glider
    class="object-cover w-full h-full"
    :media="$post->featured_image_id"
    :srcset="['1024w', '640w']"
    sizes="(max-width: 1200px) 100vw, 1024px"
    format="webp"
    quality="80"
    loading="lazy"
/>

Pass :media as either an ID or a Media instance. Use glide="..." when you'd rather pass a raw Glide query string than dozens of attributes. srcset requires sizes to also be set.

Curations for repeated crops. Define presets once in a service provider so editors pick from a fixed list instead of reinventing focal points:

use Awcodes\Curator\Curations\CurationPreset;
use Awcodes\Curator\Facades\Curation;

Curation::presets([
    CurationPreset::make('Thumbnail')->width(200)->height(200)->format('webp')->quality(80),
    CurationPreset::make('Hero')->width(1600)->height(900)->format('webp'),
]);

Presets are referenced by key when rendering Curations in Blade — this is how you keep a card grid consistent without hardcoding dimensions at every call site.

Path generators for organized storage. Swap globally in config or per-field:

CuratorPicker::make('image')->pathGenerator(DatePathGenerator::class);

DefaultPathGenerator = disk/directory, DatePathGenerator = disk/directory/Y/m/d, UserPathGenerator = disk/directory/{user-id}. Custom ones just implement PathGenerator::getPath(?string $baseDir). UserPathGenerator pairs naturally with ->tenantAware() in multi-tenant panels.

RichEditor attachments. Drop the plugin in and add the tool — editors then insert media inline without leaving the editor:

RichEditor::make('content')
    ->tools(['attachCuratorMedia'])
    ->plugins([AttachCuratorMediaPlugin::make()]),

Standalone forms package. If you're not using Filament Panels, the modal isn't auto-injected — add <x-curator::modals.modal /> before </body> in your layout. Missing this is the #1 "picker opens nothing" cause.

Config vs. per-field. Anything set on the plugin/panel or in curator.php is a global default; anything on CuratorPicker overrides it for that field. Set sane global disk/directory/accepted types once, then override sizing and validation per field only where it differs.

Gotchas and Tips

It does not work with Spatie Media Library. This is the single biggest architectural decision. If your app already uses Spatie for media, you cannot bolt Curator on top of the same models — pick one. Curator owns its own Media model and a single media table.

Token rotation is destructive. php artisan curator:token overwrites the existing value, which instantly 403s every previously signed URL: cached HTML, CDN copies, sent emails, URLs pasted into stored rich content. If you rotate, run php artisan config:clear when config is cached, and plan for a cache purge. Conversely, a missing CURATOR_GLIDE_TOKEN fails on both URL generation and the media route — so a blank .env.example value that nobody fills in on production is a hard outage, not a soft fallback.

403s are almost always token or signature related, not permissions. Before debugging the disk, verify the app that rendered the URL and the app serving it share the same token. Glide's signature includes the transformation params, so mutating a query string by hand also produces a 403 — use the component and let it sign.

Accepted file types: the defaults are a security boundary, not a suggestion. MimeType::defaults() deliberately excludes text/html, text/xml, application/xml, JS, PHP, shell, SWF and application/octet-stream. These are excluded because media is served from your app's own origin — an uploaded HTML file with a <script> runs with the opener's session. application/octet-stream is excluded because finfo returns it for anything unclassifiable, so allowing it is a wildcard.

If you genuinely need to host those types:

CuratorPicker::make('attachment')
    ->acceptedFileTypes([...MimeType::defaults(), 'text/html']);

…but serve them from a private disk. Curator's route adds X-Content-Type-Options: nosniff and forces Content-Disposition: attachment on restricted types — however, files on the public disk are also reachable directly through the storage symlink, where those headers do not apply. Only SVG uploads are sanitized; everything else you opt into is stored and served verbatim.

CuratorColumn with multiple() will N+1 hard. Eager load on every table query. Also note ->limit(), ->ring() and ->overlap() are display-only — they don't reduce the queries, only what's rendered.

Glide performance on remote disks. force on <x-curator-glider> forces a signed URL even when Curator could serve a direct URL — needed on cloud disks but carries real performance cost (every image goes through Glide). Use it only where it's required, not as a blanket default.

Path generator changes are forward-only. Switching to DatePathGenerator doesn't relocate existing files; old media keeps rendering from its stored path. Plan a migration if you need consistency, and be aware that changing disk/directory in config breaks existing records the same way.

Don't forget php artisan storage:link if you're on the public disk with the default path generator — a 404 on an otherwise correctly signed URL usually means the symlink is missing or points at the wrong place.

fallback on glider must be a registered GliderFallback — passing a raw path or URL silently won't resolve. Register them explicitly in a provider, then reference by handle.

Extension points worth learning: PathGenerator (custom storage layout), CurationPreset (standardized crops), GliderFallback (brand placeholder images), CuratorPlugin fluent methods (curations(), fileSwap(), registerNavigation(), showBadge()) for panel-level behavior, and the published config/curator.php for everything global.

Version pinning matters: v5.x supports Filament 4.x and 5.x; v4.x is Filament 4.x only, v3.x is Filament 3.x only. Mismatching these produces confusing component-not-found and Livewire errors that look like installation failures but are just compatibility skew.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
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