August 30, 2026
Upload ke Cloudinary Sukses, tapi Gambarnya Ilang Pas Dibuka Lagi di Filament? Ini Biang Keroknya
Haiii teman-teman, balik lagi bareng aku! Kalau di tulisan sebelumnya aku udah bahas cara pakai Cloudinary buat nyimpen foto & video diโฆ

By Wahyu Ivan
6 min read
Haiii teman-teman, balik lagi bareng aku! Kalau di tulisan sebelumnya aku udah bahas cara pakai Cloudinary buat nyimpen foto & video di project Laravel biasa, kali ini aku mau lanjutin ke kasus yang sedikit beda: gimana kalau CRUD-nya nggak kita bikin manual, tapi pakai Filament?
Ceritanya, ada klien yang cerita ke aku kejadian yang lumayan bikin pusing di halaman berita yang dia punya: bikin berita baru, upload beberapa gambar, terus simpan โ begitu tersimpan, gambar-gambar itu malah langsung hilang dari form, padahal belum ke mana-mana, masih di halaman yang sama. Udah gitu, dicek ke database, semua gambar yang di-upload ternyata tercatat lengkap. Dicek ke Cloudinary, filenya juga beneran ada di sana. Datanya aman semua, cuma UI-nya yang nggak mau nampilin.
Makin runyam pas dia coba buka lagi berita itu buat di-edit โ gambar yang udah tersimpan tetep aja nggak muncul. Alhasil mau ganti gambar, mau hapus, atau mau nambah, semuanya jadi buntu karena field-nya kelihatan kosong melompong. Yang bikin dia yakin ini bukan salah konfigurasi Laravel-nya secara umum: begitu storage-nya dibalikin ke hosting biasa (bukan Cloudinary), modul yang sama langsung jalan normal lagi. Jadi titik masalahnya jelas ada di kombinasi Cloudinary + Filament-nya.
Aku penasaran, jadi aku bikinin project kecil buat reproduce masalah ini sekaligus nyari solusinya. Ternyata ada 3 hal yang bikin kombinasi Filament + Cloudinary ini rewel kalau nggak disetup dengan benar. Tulisan ini bakal bahas ketiganya, plus contoh project lengkapnya.
Kalau kalian belum familiar, Filament itu panel builder buat Laravel โ tinggal define resource (model apa yang mau di-CRUD), Filament yang generate seluruh halaman list, create, dan edit-nya otomatis, termasuk komponen upload file lewat field FileUpload. Nah, FileUpload inilah yang jadi pusat masalah kita hari ini kalau disandingkan sama Cloudinary sebagai disk penyimpanannya.
1. Setup Akun Cloudinary
Bagian ini persis kayak di tulisan sebelumnya, jadi aku nggak ulang detailnya di sini โ cek tulisan lama aku buat cara bikin akun dan nemuin Cloud Name, API Key, dan API Secret di halaman Dashboard Cloudinary kalian. Tiga kredensial itu yang bakal kita pakai di project Laravel + Filament.
2. Instalasi dan Setup Package Cloudinary untuk Laravel
Bedanya sama tulisan sebelumnya, kali ini aku pakai package codebar-ag/laravel-flysystem-cloudinary. Kenapa bukan cloudinary-labs/cloudinary-laravel kayak dulu? Karena package itu cuma nyediain facade Cloudinary::upload() buat dipanggil manual โ sedangkan yang kita perlukan sekarang adalah Cloudinary yang kedaftar sebagai disk filesystem Laravel yang sesungguhnya, biar Filament FileUpload bisa langsung pakai lewat ->disk('cloudinary') tanpa kita perlu nulis kode upload/hapus manual sendiri.
composer require codebar-ag/laravel-flysystem-cloudinarycomposer require codebar-ag/laravel-flysystem-cloudinaryPackage ini auto-discovery, jadi kalian tinggal daftarin disk-nya di config/filesystems.php:
'cloudinary' => [
'driver' => 'cloudinary',
'cloud_name' => env('CLOUDINARY_CLOUD_NAME'),
'api_key' => env('CLOUDINARY_API_KEY'),
'api_secret' => env('CLOUDINARY_API_SECRET'),
'url' => [
'secure' => (bool) env('CLOUDINARY_SECURE_URL', true),
],
],'cloudinary' => [
'driver' => 'cloudinary',
'cloud_name' => env('CLOUDINARY_CLOUD_NAME'),
'api_key' => env('CLOUDINARY_API_KEY'),
'api_secret' => env('CLOUDINARY_API_SECRET'),
'url' => [
'secure' => (bool) env('CLOUDINARY_SECURE_URL', true),
],
],Terus isi kredensialnya di .env:
FILESYSTEM_DISK=cloudinary
CLOUDINARY_CLOUD_NAME=your-cloud-name
CLOUDINARY_API_KEY=your-api-key
CLOUDINARY_API_SECRET=your-api-secretFILESYSTEM_DISK=cloudinary
CLOUDINARY_CLOUD_NAME=your-cloud-name
CLOUDINARY_API_KEY=your-api-key
CLOUDINARY_API_SECRET=your-api-secretSelesai, nggak perlu edit app.php atau daftarin facade apa pun โ beda sama versi sebelumnya yang manual. Sekarang kita masuk ke Filament-nya.
3. Implementasi di Filament
Case study yang aku pakai kali ini adalah modul Berita, dengan field title, slug, content, dan images (bisa upload lebih dari satu gambar sekaligus). Migration-nya kurang lebih begini:
Schema::create('beritas', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug')->unique();
$table->text('content');
$table->json('images')->nullable();
$table->timestamps();
});Schema::create('beritas', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug')->unique();
$table->text('content');
$table->json('images')->nullable();
$table->timestamps();
});Kolom images aku simpan sebagai JSON array berisi path-path relatif (bukan tabel media terpisah โ buat kasus ini satu kolom JSON udah cukup). Di model Berita, tinggal cast jadi array:
protected $casts = [
'images' => 'array',
];protected $casts = [
'images' => 'array',
];Nah, bagian intinya ada di form Filament-nya:
FileUpload::make('images')
->image()
->multiple()
->disk('cloudinary')
->directory('berita')
->visibility('public')
->reorderable(),FileUpload::make('images')
->image()
->multiple()
->disk('cloudinary')
->directory('berita')
->visibility('public')
->reorderable(),Kalau cuma ditulis sampai sini, upload-nya bakal jalan mulus โ foto masuk ke Cloudinary, path-nya kesimpen di database. Coba jalanin, upload beberapa gambar, dan hasilnya bakal kelihatan kayak gini:
Kelihatannya udah beres, kan? Ini dia yang bikin jebakan โ begitu disimpan dan dibuka lagi buat edit, di sinilah 3 masalah yang aku sebut di awal mulai muncul.
4. Tiga Masalah yang Bikin Gambar "Hilang" (dan Solusinya)
Masalah #1: Filament nyimpen path, bukan URL
FileUpload itu didesain buat nyimpen path relatif di sebuah disk, bukan URL lengkap. Waktu field itu perlu nampilin ulang gambar yang udah tersimpan (misalnya pas form edit dibuka), Filament nggak baca kolom images terus tampilin apa adanya โ dia manggil Storage::disk($disk)->url($path) sendiri buat generate ulang URL preview-nya dari path yang tersimpan.
Godaan yang sering kejadian: nyimpen secure_url hasil balikan Cloudinary API langsung ke database, soalnya kelihatan paling gampang. Begitu itu terjadi, Storage::disk($disk)->url($path) dikasih makan URL lengkap (https://res.cloudinary.com/...) seolah-olah itu path โ hasilnya bukan URL yang valid, dan preview-nya gagal render. Ini juga udah pernah dilaporkan sebagai issue di repo Filament.
Solusinya: jangan simpan secure_url secara manual sama sekali. Karena Cloudinary sudah kedaftar sebagai disk asli (langkah 2 di atas), biarkan FileUpload yang nyimpen apa pun yang dibalikin disk driver-nya โ itu tugas Filament, bukan tugas kode kita.
Masalah #2: Cloudinary dobel ekstensi kalau nama file-nya bawa ekstensi
Ini yang lebih jarang kebahas, dan spesifik ke Cloudinary. Setelah disk-nya bener, aku masih nemu previewnya 404 pas dicoba beneran. Setelah aku lacak sampai ke kode adapternya, ternyata setiap kali manggil Storage::disk('cloudinary')->put($path, $contents), isi $path itu dijadiin public_id di Cloudinary apa adanya โ kalau path-nya berita/foto.jpg, maka public_id-nya string berita/foto.jpg utuh, bukan berita/foto dengan format .jpg. Pas Cloudinary generate delivery URL, dia nempelin lagi ekstensi hasil deteksi formatnya di belakang, jadi:
https://res.cloudinary.com/<cloud>/image/upload/v.../berita/foto.jpg.jpghttps://res.cloudinary.com/<cloud>/image/upload/v.../berita/foto.jpg.jpgDobel ekstensi. Sementara path yang kesimpen di database (berita/foto.jpg) kalau dipakai buat generate URL preview, hasilnya nunjuk ke .../berita/foto.jpg โ yang nggak pernah ada. 404.
Solusinya: pastikan nama file yang kesimpen nggak punya ekstensi sama sekali. Filament punya method getUploadedFileNameForStorageUsing() buat override nama file default-nya:
FileUpload::make('images')
->image()
->multiple()
->disk('cloudinary')
->directory('berita')
->visibility('public')
->reorderable()
->getUploadedFileNameForStorageUsing(
fn (): string => (string) Str::ulid(),
),FileUpload::make('images')
->image()
->multiple()
->disk('cloudinary')
->directory('berita')
->visibility('public')
->reorderable()
->getUploadedFileNameForStorageUsing(
fn (): string => (string) Str::ulid(),
),Aku cek langsung lewat Cloudinary Admin API โ dengan nama file tanpa ekstensi, public_id yang kesimpen persis sama dengan path yang di-generate ulang buat preview, dan gambarnya nongol normal.
Masalah #3: Render gambar lama-nya lambat, gampang dikira gagal
Ini yang paling bikin bingung waktu ditest langsung โ dua masalah di atas udah dibenerin, tapi pas form edit dibuka, field gambarnya sempet kelihatan kayak gini dulu:
Ini bukan berarti gagal โ cuma butuh waktu buat widget-nya selesai "nge-load" gambar lamanya. Masalahnya, waktu yang dibutuhkan itu bisa nyampe belasan detik kalau gambarnya lebih dari satu, dan wajar banget kalau keburu disimpulkan "ini nggak jalan" sebelum sempat selesai.
Penyebabnya: buat setiap file yang udah tersimpan, defaultnya Filament manggil Cloudinary Admin API tiga kali terpisah โ sekali buat cek file-nya ada (exists), sekali buat ambil ukuran file (size), sekali lagi buat ambil tipe file-nya (mimeType). Padahal ketiga informasi itu sebenarnya bisa didapat dari satu response API yang sama. Kalau ada 2 gambar, itu jadi 6 request Cloudinary yang jalan satu-satu (bukan paralel).
Karena field ini sudah pasti isinya gambar (dikunci lewat ->image()) dan URL-nya bisa dihitung sendiri tanpa perlu nanya ke Cloudinary, aku skip semua panggilan API itu:
FileUpload::make('images')
->image()
->multiple()
->disk('cloudinary')
->directory('berita')
->visibility('public')
->reorderable()
->getUploadedFileNameForStorageUsing(
fn (): string => (string) Str::ulid(),
)
->fetchFileInformation(false)
->getUploadedFileUsing(fn (string $file, string|array|null $storedFileNames, BaseFileUpload $component): array => [
'name' => ($component->isMultiple() ? ($storedFileNames[$file] ?? null) : $storedFileNames) ?? basename($file),
'size' => 0,
'type' => 'image',
'url' => Storage::disk('cloudinary')->url($file),
]),FileUpload::make('images')
->image()
->multiple()
->disk('cloudinary')
->directory('berita')
->visibility('public')
->reorderable()
->getUploadedFileNameForStorageUsing(
fn (): string => (string) Str::ulid(),
)
->fetchFileInformation(false)
->getUploadedFileUsing(fn (string $file, string|array|null $storedFileNames, BaseFileUpload $component): array => [
'name' => ($component->isMultiple() ? ($storedFileNames[$file] ?? null) : $storedFileNames) ?? basename($file),
'size' => 0,
'type' => 'image',
'url' => Storage::disk('cloudinary')->url($file),
]),Hasilnya di project ini, waktu render gambar yang sudah ada turun dari sekitar 11 detik jadi di bawah setengah detik โ aku ukur langsung lewat browser beneran, bukan cuma dugaan. Sekarang tampilannya jadi begini, cepet dan gambarnya langsung muncul:
Bonus: Edit Gambar Spesifik Kalau Upload Lebih dari Satu
Kalian sempet nanya, "kalau upload lebih dari satu gambar, gimana cara ganti/hapus salah satunya aja?" โ ternyata ini udah otomatis didukung Filament tanpa tambahan kode apa pun. Tiap gambar yang udah ke-upload punya tombol hapus (ร) sendiri-sendiri, dan ->reorderable() yang udah kita pasang dari awal bikin urutannya juga bisa di-drag.
Coba hapus salah satu terus simpan, dan cuma gambar yang dihapus itu yang hilang dari database โ gambar yang lain tetap aman:
Mau ganti satu gambar tinggal hapus yang lama, upload yang baru di posisi yang sama.
Kesimpulan
Gejala "gambar hilang setelah save" waktu pakai Filament + Cloudinary hampir selalu soal 3 hal:
- Simpan path relatif dari disk driver, jangan pernah simpan
secure_urlmanual. - Path itu nggak boleh bawa ekstensi file, khusus buat disk Cloudinary โ biar
public_iddi Cloudinary dan path di database persis sama. - Kalau field-nya kelihatan kosong/lambat, cek dulu berapa kali API Cloudinary dipanggil buat setiap file โ defaultnya bisa 3x per file yang jelas-jelas nggak perlu kalau kalian sudah tau tipe file-nya dari awal.
Ikutin tiga hal itu, dan integrasi Cloudinary sebagai media manager di Filament bisa jalan mulus tanpa kode tambahan yang aneh-aneh. Contoh project lengkapnya (Laravel + Filament + resource Berita dengan multi-image upload ke Cloudinary, plus test otomatisnya) ada di repositori ini.
Sekian pembahasan kali ini soal integrasi Cloudinary sebagai media manager di Filament. Semoga bermanfaat buat kalian yang lagi ngalamin masalah serupa, dan terimakasih sudah membaca!
Reference: [1] https://filamentphp.com/docs/forms/fields/file-upload [2] https://github.com/filamentphp/filament/issues/7669 [3] https://github.com/codebar-ag/laravel-flysystem-cloudinary [4] https://cloudinary.com/documentation/admin_api