Blog tanpa gambar terasa hambar. Episode ini kita tambahkan fitur upload thumbnail — dan sekalian belajar bagaimana Laravel mengelola file storage.
Post baru berhasil dibuat lengkap dengan thumbnail hasil upload.
Kenapa perlu storage:link?
Laravel menyimpan file upload di storage/app/public, bukan langsung di folder public/ — ini demi keamanan dan organisasi. Supaya file itu bisa diakses lewat browser, kita perlu symbolic link dari public/storage ke storage/app/public:
php artisan storage:link
INFO The [public/storage] link has been connected to [storage/app/public].
Perintah ini cukup dijalankan sekali per environment (development, staging, production masing-masing perlu link sendiri).
Validasi dan simpan file di store()
$validated = $request->validate([
'category_id' => ['nullable', 'exists:categories,id'],
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
'thumbnail' => ['nullable', 'image', 'max:2048'],
]);
$validated['slug'] = str($validated['title'])->slug();
if ($request->hasFile('thumbnail')) {
$validated['thumbnail'] = $request->file('thumbnail')->store('thumbnails', 'public');
}
$post = Post::create($validated);
'image', 'max:2048'— rule validasi bawaan: harus file gambar, maksimal 2MB (satuan kilobyte)$request->file('thumbnail')->store('thumbnails', 'public')— simpan file ke folderthumbnailsdi diskpublic, otomatis generate nama file unik, dan mengembalikan path relatifnya (misalthumbnails/xyz123.jpg) untuk disimpan di kolomthumbnail
Update: hapus file lama sebelum ganti yang baru
if ($request->hasFile('thumbnail')) {
if ($post->thumbnail) {
Storage::disk('public')->delete($post->thumbnail);
}
$validated['thumbnail'] = $request->file('thumbnail')->store('thumbnails', 'public');
}
Tanpa langkah ini, setiap kali user upload thumbnail baru, file lama akan menumpuk terus di storage tanpa pernah terhapus — kebocoran storage klasik.
Delete: hapus file saat post dihapus
public function destroy(Post $post)
{
if ($post->thumbnail) {
Storage::disk('public')->delete($post->thumbnail);
}
$post->delete();
return redirect()->route('posts.index')->with('success', 'Post deleted successfully.');
}
Form dengan enctype="multipart/form-data"
Satu detail yang sering terlewat: form HTML tidak bisa upload file kecuali attribute enctype-nya diset:
<form method="POST" action="{{ route('posts.store') }}" enctype="multipart/form-data">
@csrf
<input type="file" name="thumbnail" id="thumbnail" class="form-control" accept="image/*">
<!-- ... -->
</form>
Tanpa enctype="multipart/form-data", file yang dipilih user tidak akan pernah sampai ke server — validasi hasFile('thumbnail') akan selalu false, dan ini sering bikin bingung karena tidak ada error yang jelas.
Menampilkan thumbnail
@if ($post->thumbnail)
<img src="{{ asset('storage/' . $post->thumbnail) }}" alt="{{ $post->title }}" class="img-fluid mb-3">
@endif
asset('storage/...') menghasilkan URL publik lewat symbolic link yang kita buat di awal.
Testing
Coba upload gambar lewat form create, lalu cek filenya benar-benar tersimpan:
ls storage/app/public/thumbnails/
Dan pastikan bisa diakses lewat browser di http://laravel13-blog-dasar.test/storage/thumbnails/<nama-file>.jpg.
Selanjutnya
Kita sudah punya relasi category_id, tapi belum ada cara untuk menjelajah post berdasarkan kategori. Episode berikutnya kita bikin halaman filter kategori.