Di tiga bagian sebelumnya kita sudah membangun fondasi yang kuat — instalasi, routing, dan controller. Sekarang saatnya membahas bagian yang paling terlihat oleh pengguna: tampilan. Blade adalah templating engine bawaan Laravel yang memungkinkan kamu menulis tampilan HTML yang dinamis, terorganisir, dan reusable — dengan sintaks yang jauh lebih bersih dan ekspresif dibanding PHP murni.
💡 Ini adalah bagian keempat dari seri "Belajar Laravel dari Nol". Pastikan kamu sudah menyelesaikan Bagian 1 (Instalasi), Bagian 2 (Routing), dan Bagian 3 (Controller) sebelum melanjutkan, karena kita akan langsung menggunakan Controller dan Route yang sudah dipelajari untuk mengirim data ke Blade template.
Apa Itu Blade dan Mengapa Lebih Baik dari PHP Murni?
Blade adalah templating engine yang dikompilasi menjadi kode PHP biasa dan di-cache — artinya tidak ada overhead performa dibanding menulis PHP murni. Yang membedakan Blade adalah sintaksnya yang jauh lebih bersih dan readable, dilengkapi fitur-fitur powerful seperti template inheritance, component, slot, dan berbagai direktif yang menghilangkan boilerplate HTML yang berulang.
// ── PHP MURNI (cara lama, verbose dan berantakan) ─────
<?php if ($user->isAdmin()): ?>
<p>Selamat datang, Admin <?php echo htmlspecialchars($user->name); ?>!</p>
<?php endif; ?>
<?php foreach ($posts as $post): ?>
<h2><?php echo htmlspecialchars($post->title); ?></h2>
<?php endforeach; ?>
// ── BLADE (bersih, aman, dan mudah dibaca) ────────────
@if ($user->isAdmin())
<p>Selamat datang, Admin {{ $user->name }}!</p>
@endif
@foreach ($posts as $post)
<h2>{{ $post->title }}</h2>
@endforeach
"Blade does not restrict you from using plain PHP code in your views. All Blade templates are compiled into plain PHP code and cached — so Blade adds zero overhead to your application."
Menampilkan Data: Sintaks {{ }} dan {!! !!}
Cara paling dasar menggunakan Blade adalah menampilkan nilai variabel yang dikirim dari Controller. Blade menyediakan dua sintaks untuk ini yang memiliki perbedaan keamanan yang sangat penting dan harus dipahami dengan benar sebelum digunakan.
Double curly braces {{ $variabel }} adalah cara standar menampilkan variabel di Blade. Semua output otomatis di-escape menggunakan htmlspecialchars() untuk mencegah XSS (Cross-Site Scripting) attack. Gunakan ini untuk hampir semua kasus — terutama untuk data yang berasal dari input pengguna.
Triple braces atau exclamation syntax {!! $variabel !!} menampilkan output TANPA escaping — artinya tag HTML di dalam variabel akan dirender sebagai HTML oleh browser. Gunakan HANYA untuk konten yang sudah terjamin aman, seperti konten dari WYSIWYG editor yang sudah di-sanitize di backend, bukan dari input pengguna langsung.
Gunakan operator null coalescing ?? di dalam sintaks Blade untuk menampilkan nilai default jika variabel null atau tidak terdefinisi — cara yang elegan untuk menghindari error saat variabel opsional tidak tersedia.
{{-- resources/views/posts/show.blade.php --}}
{{-- ── MENAMPILKAN VARIABEL (auto-escaped, aman) ──── --}}
<h1>{{ $post->title }}</h1>
<p>Ditulis oleh: {{ $post->user->name }}</p>
<p>Kategori: {{ $post->category->name }}</p>
<p>Tanggal: {{ $post->created_at->format('d F Y') }}</p>
{{-- ── HTML MENTAH (HANYA jika konten sudah aman) ── --}}
{{-- Konten dari rich text editor yang sudah di-sanitize --}}
<div class="prose">
{!! $post->content !!}
</div>
{{-- ── NILAI DEFAULT DENGAN ?? ──────────────────── --}}
<p>Excerpt: {{ $post->excerpt ?? 'Tidak ada excerpt.' }}</p>
<p>Tags: {{ $post->tags_string ?? 'Belum ada tag' }}</p>
{{-- ── EKSPRESI PHP DI DALAM {{ }} ─────────────── --}}
<p>Total komentar: {{ $post->comments->count() }}</p>
<p>Dibaca: {{ number_format($post->views) }} kali</p>
<p>Status: {{ $post->status === 'published' ? 'Dipublikasikan' : 'Draft' }}</p>
<p>Waktu lalu: {{ $post->created_at->diffForHumans() }}</p>
{{-- ── ESCAPE BLADE SYNTAX (tampilkan {{ literal }}) ── --}}
Gunakan @{{ variabel }} jika tidak ingin diproses Blade
{{-- Output: {{ variabel }} --}}
Direktif Kondisi: @if, @unless, @isset, @empty
Direktif kondisi Blade memungkinkan tampilan bereaksi secara dinamis berdasarkan data yang diterima dari Controller. Blade menyediakan berbagai direktif kondisi yang masing-masing dirancang untuk kasus penggunaan yang berbeda — membuatnya lebih ekspresif dan mudah dibaca dibanding if-else PHP biasa.
{{-- ── @if / @elseif / @else / @endif ──────────────── --}}
@if ($post->status === 'published')
<span class="badge-green">Dipublikasikan</span>
@elseif ($post->status === 'draft')
<span class="badge-yellow">Draft</span>
@else
<span class="badge-gray">Arsip</span>
@endif
{{-- ── @unless (kebalikan @if) ──────────────────────── --}}
{{-- "tampilkan ini KECUALI kondisi benar" --}}
@unless ($post->is_published)
<div class="banner-warning">
Post ini belum dipublikasikan dan hanya bisa dilihat olehmu.
</div>
@endunless
{{-- ── @isset (cek apakah variabel terdefinisi & tidak null) ── --}}
@isset($post->thumbnail)
<img src="{{ Storage::url($post->thumbnail) }}" alt="{{ $post->title }}">
@endisset
{{-- ── @empty (cek apakah variabel kosong / falsy) ───── --}}
@empty($post->tags)
<p class="text-gray-400">Belum ada tag untuk post ini.</p>
@endempty
{{-- ── @auth dan @guest ───────────────────────────────── --}}
{{-- Sangat berguna untuk navigasi yang berbeda per status login --}}
@auth
<a href="{{ route('dashboard') }}">Dashboard</a>
<form action="/logout" method="POST">
@csrf
<button type="submit">Logout</button>
</form>
@endauth
@guest
<a href="{{ route('login') }}">Login</a>
<a href="{{ route('register') }}">Daftar</a>
@endguest
{{-- ── @env (cek environment saat ini) ──────────────── --}}
@env('local')
<div class="debug-bar">DEBUG MODE AKTIF</div>
@endenv
Direktif Perulangan: @foreach, @forelse, @for, @while
Menampilkan daftar data adalah salah satu hal paling sering dilakukan di template. Blade menyediakan beberapa direktif perulangan yang tidak hanya lebih bersih dari PHP biasa, tapi juga memiliki fitur tambahan yang sangat berguna — terutama variabel $loop yang memberikan informasi tentang iterasi yang sedang berjalan.
{{-- ── @foreach / @endforeach ───────────────────────── --}}
<ul>
@foreach ($posts as $post)
<li>
<a href="{{ route('posts.show', $post) }}">
{{ $post->title }}
</a>
<span>{{ $post->created_at->format('d M Y') }}</span>
</li>
@endforeach
</ul>
{{-- ── VARIABEL $loop ─────────────────────────────────── --}}
{{-- Tersedia otomatis di dalam @foreach --}}
@foreach ($posts as $post)
<div class="
@if ($loop->first) border-t @endif
@if ($loop->last) border-b @endif
">
{{-- $loop->index → indeks saat ini (mulai dari 0) --}}
{{-- $loop->iteration → nomor urut saat ini (mulai dari 1) --}}
{{-- $loop->count → total item dalam loop --}}
{{-- $loop->first → true jika iterasi pertama --}}
{{-- $loop->last → true jika iterasi terakhir --}}
{{-- $loop->even → true jika iterasi genap --}}
{{-- $loop->odd → true jika iterasi ganjil --}}
{{-- $loop->remaining → sisa iterasi --}}
{{-- $loop->depth → kedalaman nested loop --}}
{{-- $loop->parent → variabel $loop dari loop luar --}}
<span>{{ $loop->iteration }}/{{ $loop->count }}</span>
<h3>{{ $post->title }}</h3>
{{-- Alternating row color --}}
<div class="{{ $loop->even ? 'bg-gray-50' : 'bg-white' }}">
{{ $post->excerpt }}
</div>
</div>
@endforeach
{{-- ── @forelse (foreach + kondisi jika kosong) ──────── --}}
{{-- Lebih bersih dari @foreach + @if $collection->isEmpty() --}}
@forelse ($posts as $post)
<div class="post-card">
<h3>{{ $post->title }}</h3>
<p>{{ $post->excerpt }}</p>
</div>
@empty
{{-- Ditampilkan jika $posts kosong --}}
<div class="empty-state">
<p>Belum ada post yang dipublikasikan.</p>
<a href="{{ route('posts.create') }}">Tulis Post Pertama</a>
</div>
@endforelse
{{-- ── @for ───────────────────────────────────────────── --}}
@for ($i = 1; $i <= 5; $i++)
<div class="rating-star {{ $i <= $product->rating ? 'filled' : '' }}">
★
</div>
@endfor
{{-- ── @while ──────────────────────────────────────────── --}}
@while ($item = array_shift($items))
<li>{{ $item }}</li>
@endwhile
{{-- ── @break dan @continue ───────────────────────────── --}}
@foreach ($posts as $post)
@continue($post->status !== 'published')
{{-- Lewati post yang bukan published --}}
<h3>{{ $post->title }}</h3>
@break($loop->iteration === 5)
{{-- Berhenti setelah 5 post --}}
@endforeach
Template Inheritance: Layout yang Reusable
Salah satu fitur paling powerful di Blade adalah template inheritance — sistem yang memungkinkan kamu mendefinisikan layout utama sekali, lalu setiap halaman "mewarisi" layout tersebut dan hanya mengisi bagian yang berbeda. Ini menghilangkan duplikasi HTML yang massif seperti menulis ulang header, navigasi, dan footer di setiap file view.
Layout utama adalah kerangka HTML yang digunakan bersama oleh semua halaman. Di dalamnya kamu mendefinisikan @yield('nama-section') sebagai placeholder — area yang akan diisi oleh halaman turunan dengan konten spesifiknya masing-masing.
Halaman turunan menggunakan @extends('nama.layout') untuk mewarisi layout, lalu @section('nama-section') ... @endsection untuk mengisi placeholder yang sudah didefinisikan di layout. Semua HTML di luar @section akan diabaikan.
@yield() bisa menerima parameter kedua sebagai nilai default yang ditampilkan jika halaman turunan tidak mendefinisikan section tersebut. Sangat berguna untuk nilai seperti title halaman atau meta description yang boleh berbeda per halaman tapi punya fallback yang masuk akal.
{{-- resources/views/layouts/app.blade.php --}}
{{-- Layout utama yang digunakan semua halaman --}}
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
{{-- @yield dengan nilai default --}}
<title>@yield('title', config('app.name')) | DevPath</title>
<meta name="description" content="@yield('meta-description', 'Platform belajar programming Indonesia')">
{{-- Asset CSS dikompilasi Vite --}}
@vite(['resources/css/app.css', 'resources/js/app.js'])
{{-- Section untuk CSS tambahan per halaman --}}
@stack('styles')
</head>
<body class="bg-gray-50 text-gray-900">
{{-- Navigasi --}}
@include('partials.navbar')
{{-- Flash Message --}}
@include('partials.flash-message')
{{-- Konten utama — diisi oleh halaman turunan --}}
<main class="min-h-screen">
@yield('content')
</main>
{{-- Footer --}}
@include('partials.footer')
{{-- Section untuk JavaScript tambahan per halaman --}}
@stack('scripts')
</body>
</html>
{{-- resources/views/posts/index.blade.php --}}
{{-- Halaman yang mewarisi layout --}}
@extends('layouts.app')
{{-- Isi section 'title' --}}
@section('title', 'Semua Artikel')
@section('meta-description', 'Kumpulan artikel programming dan teknologi terbaru')
{{-- Isi section 'content' — inilah yang ditampilkan di <main> --}}
@section('content')
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold">Semua Artikel</h1>
@auth
<a href="{{ route('posts.create') }}"
class="btn-primary">
+ Tulis Artikel
</a>
@endauth
</div>
{{-- Grid artikel --}}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@forelse ($posts as $post)
@include('posts.partials.card', ['post' => $post])
@empty
<div class="col-span-3 text-center py-16">
<p class="text-gray-500">Belum ada artikel.</p>
</div>
@endforelse
</div>
{{-- Pagination --}}
<div class="mt-8">
{{ $posts->links() }}
</div>
</div>
@endsection
{{-- Tambahkan CSS spesifik untuk halaman ini --}}
@push('styles')
<style>
.post-grid { display: grid; }
</style>
@endpush
{{-- Tambahkan JS spesifik untuk halaman ini --}}
@push('scripts')
<script>
console.log('Halaman posts/index loaded');
</script>
@endpush
@include: Memecah Template Menjadi Bagian Kecil
Seiring aplikasi berkembang, file Blade bisa menjadi sangat panjang dan sulit dibaca. Direktif @include memungkinkan kamu memecah template menjadi file-file kecil yang bisa digunakan ulang di banyak tempat — mirip seperti konsep "partial" atau "fragment" di framework lain.
{{-- ── @include DASAR ────────────────────────────────── --}}
{{-- Menyertakan file view lain --}}
@include('partials.navbar')
@include('partials.footer')
@include('partials.flash-message')
{{-- ── @include DENGAN DATA TAMBAHAN ────────────────── --}}
{{-- Kirim variabel ke partial yang di-include --}}
@include('posts.partials.card', [
'post' => $post,
'showAuthor' => true,
'compact' => false,
])
{{-- ── @includeIf (hanya include jika file ada) ──────── --}}
{{-- Tidak error jika file tidak ditemukan --}}
@includeIf('partials.banner-promo')
{{-- ── @includeWhen (include berdasarkan kondisi) ────── --}}
@includeWhen(auth()->check(), 'partials.user-toolbar')
@includeWhen($post->has_video, 'posts.partials.video-player', ['post' => $post])
{{-- ── @includeUnless (include jika kondisi TIDAK benar) ── --}}
@includeUnless($user->isAdmin(), 'partials.upgrade-banner')
{{-- ── @includeFirst (include file pertama yang ditemukan) ── --}}
{{-- Cek satu per satu, gunakan yang pertama ada --}}
@includeFirst(['custom.navbar', 'partials.navbar'])
{{-- ── STRUKTUR FOLDER PARTIAL YANG BAIK ─────────────── --}}
{{--
resources/views/
├── layouts/
│ ├── app.blade.php ← Layout utama
│ ├── admin.blade.php ← Layout admin
│ └── auth.blade.php ← Layout halaman auth
├── partials/
│ ├── navbar.blade.php ← Navigasi
│ ├── footer.blade.php ← Footer
│ └── flash-message.blade.php ← Flash messages
└── posts/
├── index.blade.php
├── show.blade.php
└── partials/
├── card.blade.php ← Card post untuk grid
├── meta.blade.php ← Meta info post
└── share-buttons.blade.php
--}}
Blade Components: Cara Modern Membangun UI
Blade Components adalah evolusi dari @include — cara yang lebih powerful dan terstruktur untuk membuat elemen UI yang reusable. Berbeda dengan @include yang hanya menyertakan file view, Component bisa memiliki kelas PHP sendiri yang berisi logika, menerima props dengan type-hint, dan menggunakan slot untuk konten yang fleksibel.
Anonymous Component adalah jenis component paling sederhana — hanya berupa file Blade tanpa kelas PHP. Cocok untuk elemen UI sederhana seperti button, badge, alert, atau card yang hanya butuh menerima props dan menampilkan konten.
Class-Based Component memiliki kelas PHP yang menanggani logika sebelum data dikirim ke view. Cocok untuk component yang membutuhkan komputasi, formatting, atau interaksi dengan data sebelum ditampilkan.
Slot memungkinkan component menerima konten HTML yang ditentukan dari luar — bukan hanya nilai string sederhana. Ada default slot untuk konten utama dan named slot untuk area konten tertentu seperti header, footer, atau sidebar di dalam component.
# Buat Anonymous Component (hanya file Blade)
php artisan make:component Alert --view
# File: resources/views/components/alert.blade.php
# Buat Class-Based Component (kelas PHP + file Blade)
php artisan make:component Alert
# File: app/View/Components/Alert.php
# resources/views/components/alert.blade.php
# Buat Component di dalam subfolder
php artisan make:component Forms/Input --view
# File: resources/views/components/forms/input.blade.php
# Penggunaan: <x-forms.input />
{{-- resources/views/components/alert.blade.php --}}
{{-- Anonymous Component untuk alert/notification --}}
{{-- $attributes merging — semua attribute dari luar digabung --}}
<div {{ $attributes->merge(['class' => "rounded-lg p-4 flex items-start gap-3 $typeClass"]) }}>
<span class="text-xl">{{ $icon }}</span>
<div>
@isset($title)
<p class="font-semibold">{{ $title }}</p>
@endisset
{{-- $slot adalah konten di antara tag pembuka dan penutup --}}
<p class="text-sm">{{ $slot }}</p>
</div>
@isset($action)
<div class="ml-auto">{{ $action }}</div>
@endisset
</div>
<?php
// app/View/Components/Alert.php
// Class-Based Component dengan logika
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class Alert extends Component
{
// Props diterima di constructor
public string $typeClass;
public string $icon;
public function __construct(
public string $type = 'info', // info | success | warning | error
public ?string $title = null,
) {
// Logika ada di sini, bukan di Blade template
$this->typeClass = match($type) {
'success' => 'bg-green-50 text-green-800 border border-green-200',
'warning' => 'bg-yellow-50 text-yellow-800 border border-yellow-200',
'error' => 'bg-red-50 text-red-800 border border-red-200',
default => 'bg-blue-50 text-blue-800 border border-blue-200',
};
$this->icon = match($type) {
'success' => '✅',
'warning' => '⚠️',
'error' => '❌',
default => 'ℹ️',
};
}
public function render(): View
{
return view('components.alert');
}
}
{{-- Menggunakan Component di Blade template lain --}}
{{-- Sintaks component: <x-nama-component prop="nilai"> --}}
{{-- Alert sederhana --}}
<x-alert type="success">
Post berhasil disimpan ke database!
</x-alert>
{{-- Alert dengan title dan named slot --}}
<x-alert type="warning" title="Perhatian!">
Kamu belum mengisi bagian bio di profil.
{{-- Named slot 'action' --}}
<x-slot:action>
<a href="{{ route('profile') }}" class="underline text-sm">
Lengkapi Profil
</a>
</x-slot:action>
</x-alert>
{{-- Merge attribute dari luar --}}
<x-alert type="error" class="mt-4 mb-6" id="error-banner">
Terjadi kesalahan saat memproses pembayaran.
</x-alert>
{{-- Component lain yang umum dibuat --}}
<x-button variant="primary" size="lg" :href="route('posts.create')">
Tulis Artikel Baru
</x-button>
<x-badge color="green">Published</x-badge>
<x-badge color="yellow">Draft</x-badge>
<x-card>
<x-slot:header>
<h3>Judul Card</h3>
</x-slot:header>
Konten card ada di sini.
<x-slot:footer>
<button>Simpan</button>
</x-slot:footer>
</x-card>
@stack dan @push: Asset per Halaman
Saat membangun aplikasi dengan banyak halaman, tidak semua halaman membutuhkan CSS atau JavaScript yang sama. Memuat semua asset di setiap halaman adalah pemborosan — lebih baik hanya memuat asset yang benar-benar dibutuhkan halaman tersebut. Direktif @stack dan @push dirancang persis untuk kebutuhan ini.
{{-- Di layout utama (resources/views/layouts/app.blade.php) --}}
<head>
{{-- Asset global yang selalu dimuat --}}
@vite(['resources/css/app.css', 'resources/js/app.js'])
{{-- Placeholder untuk CSS tambahan per halaman --}}
@stack('styles')
</head>
<body>
@yield('content')
{{-- Placeholder untuk JS tambahan per halaman --}}
@stack('scripts')
</body>
{{-- ─────────────────────────────────────────────────── --}}
{{-- Di halaman yang membutuhkan asset tertentu --}}
{{-- resources/views/posts/show.blade.php --}}
@extends('layouts.app')
@section('content')
{{-- Konten halaman --}}
<div id="code-block">{{ $post->content }}</div>
@endsection
{{-- Push CSS untuk syntax highlighting --}}
@push('styles')
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
@endpush
{{-- Push JS untuk syntax highlighting --}}
@push('scripts')
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('pre code').forEach((el) => {
hljs.highlightElement(el);
});
});
</script>
@endpush
{{-- @prepend: tambahkan di AWAL stack (kebalikan @push) --}}
@prepend('styles')
<style> /* CSS ini akan muncul sebelum yang di-push lain */ </style>
@endprepend
Direktif Lainnya yang Sering Dipakai
Blade memiliki banyak direktif lain yang masing-masing menyelesaikan kebutuhan spesifik yang sering muncul saat membangun tampilan web. Berikut adalah direktif-direktif yang paling sering kamu butuhkan dalam pengembangan sehari-hari.
{{-- ── @csrf: Token Keamanan Form ──────────────────── --}}
{{-- WAJIB ada di setiap form POST/PUT/PATCH/DELETE --}}
<form action="{{ route('posts.store') }}" method="POST">
@csrf
{{-- @csrf menghasilkan: <input type="hidden" name="_token" value="..."> --}}
<button type="submit">Simpan</button>
</form>
{{-- ── @method: Override HTTP Method ─────────────────── --}}
{{-- HTML form hanya support GET dan POST --}}
{{-- Gunakan @method untuk PUT, PATCH, DELETE --}}
<form action="{{ route('posts.update', $post) }}" method="POST">
@csrf
@method('PUT')
{{-- Menghasilkan: <input type="hidden" name="_method" value="PUT"> --}}
<button type="submit">Update</button>
</form>
<form action="{{ route('posts.destroy', $post) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit" onclick="return confirm('Yakin hapus?')">Hapus</button>
</form>
{{-- ── @class: Conditional CSS Classes ────────────────── --}}
{{-- Cara bersih untuk conditional class tanpa ternary berantakan --}}
<div @class([
'px-4 py-2 rounded', // selalu ada
'bg-green-100 text-green-800' => $post->status === 'published',
'bg-yellow-100 text-yellow-800' => $post->status === 'draft',
'bg-red-100 text-red-800' => $post->status === 'archived',
'font-bold' => $post->is_featured,
])>
{{ $post->status }}
</div>
{{-- ── @selected, @checked, @disabled, @readonly ─────── --}}
<select name="category_id">
@foreach ($categories as $category)
<option value="{{ $category->id }}"
@selected($post->category_id === $category->id)>
{{ $category->name }}
</option>
@endforeach
</select>
<input type="checkbox" name="is_featured"
@checked($post->is_featured)>
<button @disabled($post->status === 'published')>
Publish
</button>
{{-- ── @php: Blok PHP di Blade ────────────────────────── --}}
{{-- Gunakan seperlunya — logika sebaiknya di Controller --}}
@php
$groupedPosts = $posts->groupBy('category.name');
$totalViews = $posts->sum('views');
@endphp
{{-- ── @dump dan @dd: Debugging ───────────────────────── --}}
{{-- Tampilkan nilai variabel untuk debugging --}}
@dump($post) {{-- dump() — lanjut eksekusi --}}
@dd($post) {{-- dd() — stop eksekusi --}}
⚠️ Selalu sertakan @csrf di dalam setiap form yang menggunakan method POST, PUT, PATCH, atau DELETE. Tanpa token CSRF, Laravel akan menolak semua form submission dengan error 419 (Page Expired). Ini adalah mekanisme keamanan bawaan Laravel untuk mencegah Cross-Site Request Forgery attack.
Praktik: Membangun Layout Lengkap untuk Blog
Mari satukan semua konsep Blade yang sudah dipelajari — layout inheritance, partial includes, component, dan berbagai direktif — dalam contoh nyata membangun tampilan blog yang lengkap dan terstruktur dengan baik.
{{-- resources/views/partials/navbar.blade.php --}}
<nav class="bg-white border-b border-gray-200 sticky top-0 z-50">
<div class="container mx-auto px-4 h-16 flex items-center justify-between">
{{-- Logo --}}
<a href="{{ route('home') }}" class="font-bold text-xl text-indigo-600">
DevPath
</a>
{{-- Menu navigasi --}}
<div class="hidden md:flex items-center gap-6">
<a href="{{ route('blog.index') }}"
class="{{ request()->routeIs('blog.*') ? 'text-indigo-600 font-semibold' : 'text-gray-600 hover:text-gray-900' }}">
Blog
</a>
<a href="{{ route('about') }}"
class="{{ request()->routeIs('about') ? 'text-indigo-600 font-semibold' : 'text-gray-600 hover:text-gray-900' }}">
Tentang
</a>
</div>
{{-- Auth section --}}
<div class="flex items-center gap-3">
@auth
<span class="text-sm text-gray-600">
Halo, {{ auth()->user()->name }}!
</span>
<a href="{{ route('dashboard') }}" class="btn-outline-sm">
Dashboard
</a>
<form action="{{ route('logout') }}" method="POST" class="inline">
@csrf
<button type="submit" class="text-sm text-gray-500 hover:text-red-500">
Logout
</button>
</form>
@endauth
@guest
<a href="{{ route('login') }}" class="btn-outline-sm">Login</a>
<a href="{{ route('register') }}" class="btn-primary-sm">Daftar</a>
@endguest
</div>
</div>
</nav>
{{-- resources/views/partials/flash-message.blade.php --}}
@if (session('success'))
<div x-data="{ show: true }" x-show="show" x-init="setTimeout(() => show = false, 4000)"
class="fixed top-4 right-4 z-50 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg">
✅ {{ session('success') }}
</div>
@endif
@if (session('error'))
<div class="fixed top-4 right-4 z-50 bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg">
❌ {{ session('error') }}
</div>
@endif
{{-- resources/views/posts/partials/card.blade.php --}}
<article class="bg-white rounded-xl border border-gray-200 overflow-hidden hover:shadow-md transition">
@if ($post->thumbnail)
<img src="{{ Storage::url($post->thumbnail) }}"
alt="{{ $post->title }}"
class="w-full h-48 object-cover">
@endif
<div class="p-5">
{{-- Kategori --}}
<a href="{{ route('blog.category', $post->category) }}"
class="text-xs font-semibold text-indigo-600 uppercase tracking-wide">
{{ $post->category->name }}
</a>
{{-- Judul --}}
<h3 class="mt-2 font-bold text-lg leading-snug">
<a href="{{ route('blog.show', $post) }}"
class="hover:text-indigo-600 transition">
{{ $post->title }}
</a>
</h3>
{{-- Excerpt --}}
<p class="mt-2 text-gray-500 text-sm line-clamp-2">
{{ $post->excerpt ?? Str::limit(strip_tags($post->content), 120) }}
</p>
{{-- Meta --}}
<div class="mt-4 flex items-center gap-3 text-xs text-gray-400">
<span>{{ $post->user->name }}</span>
<span>·</span>
<span>{{ $post->published_at->format('d M Y') }}</span>
<span>·</span>
<span>{{ $post->read_time }} menit baca</span>
</div>
</div>
</article>
💡 Perhatikan penggunaan $loop->iteration, @forelse dengan @empty, @auth/@guest, request()->routeIs() untuk active state navigasi, dan @push/@stack untuk asset per halaman di contoh-contoh di atas. Kombinasi semua fitur Blade ini adalah pola yang akan kamu gunakan berulang-ulang di setiap project Laravel — semakin sering dipakai, semakin natural rasanya.
Langkah Selanjutnya
Luar biasa — kamu sudah menguasai Blade Templating Engine secara menyeluruh! Mulai dari sintaks dasar {{ }}, direktif kondisi dan perulangan, template inheritance dengan @extends dan @yield, memecah template dengan @include, membangun Component yang reusable, hingga berbagai direktif utilitas yang membuat kode tampilan jauh lebih bersih dan terorganisir. Di bagian kelima seri ini, kita akan menyelami Migration dan Database Schema — cara Laravel mengelola struktur database dengan kode PHP yang bisa di-version control, di-rollback, dan dibagikan ke seluruh anggota tim.
🚀 Sebelum lanjut ke Bagian 5, pastikan kamu sudah berlatih: (1) membuat layout utama dengan @yield dan menggunakannya di beberapa halaman dengan @extends, (2) memecah navigasi dan footer menjadi partial terpisah dengan @include, (3) membuat minimal satu Blade Component sederhana seperti alert atau button, dan (4) menggunakan @forelse, $loop, dan @auth di template yang kamu buat. Blade adalah sesuatu yang paling cepat dikuasai dengan langsung mempraktikkannya.