M
Mr Sugiarto
Developer
10 Sep 2026 3 min read

Blog tanpa gambar terasa hambar. Episode ini kita tambahkan fitur upload thumbnail — dan sekalian belajar bagaimana Laravel mengelola file storage.

Screenshot: Post baru berhasil dibuat lengkap dengan thumbnail hasil upload. 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 folder thumbnails di disk public, otomatis generate nama file unik, dan mengembalikan path relatifnya (misal thumbnails/xyz123.jpg) untuk disimpan di kolom thumbnail

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.

M
Mr Sugiarto

Developer

Bagian dari Series: Laravel 13 Dasar — Belajar Fundamental Lewat Blog Sederhana
Lihat Series Lengkap