Di bagian sebelumnya kita sudah berhasil menginstal Laravel dan memahami struktur project secara menyeluruh. Sekarang saatnya memahami salah satu konsep paling fundamental di Laravel: Routing. Routing adalah sistem yang menentukan "jika pengguna mengakses URL ini, jalankan kode ini." Tanpa memahami routing dengan benar, kamu tidak akan bisa membangun halaman atau fitur apapun di Laravel.

💡 Ini adalah bagian kedua dari seri "Belajar Laravel dari Nol". Pastikan kamu sudah menyelesaikan Bagian 1 (Instalasi dan Struktur Project) dan project Laravel sudah berjalan di komputermu sebelum melanjutkan.


Apa Itu Routing?

Bayangkan routing seperti resepsionis di sebuah kantor besar. Setiap tamu (request dari browser) yang datang akan diterima resepsionis, ditanya tujuannya (URL yang diakses), lalu diarahkan ke ruangan yang tepat (Controller atau closure). Tanpa resepsionis, tamu tidak tahu harus ke mana — dan itulah yang terjadi jika tidak ada routing di aplikasi web.

"Laravel's router allows you to register routes that respond to any HTTP verb — making it expressive and intuitive to define your application's URLs."

— — Dokumentasi Resmi Laravel

File Route di Laravel

Laravel memisahkan definisi route ke dalam beberapa file berdasarkan jenisnya. Masing-masing file sudah di-load secara otomatis oleh framework dengan konfigurasi middleware yang berbeda. Memahami perbedaan antar file route ini penting agar kamu tidak salah menaruh definisi route.

alu-routes.txt
Text
routes/
├── web.php      ← Route untuk halaman web (browser)
│                  Sudah dilengkapi middleware: web
│                  (session, cookies, CSRF protection)
│                  Gunakan ini untuk halaman HTML biasa
│
├── api.php      ← Route untuk REST API
│                  Middleware: api (stateless, rate limiting)
│                  URL otomatis di-prefix dengan /api/
│                  Gunakan ini untuk endpoint yang dikonsumsi
│                  oleh mobile app atau JavaScript frontend
│
├── console.php  ← Route untuk Artisan commands kustom
│                  Bukan HTTP route — untuk perintah CLI
│
└── channels.php ← Route untuk broadcasting (WebSocket)
                   Digunakan bersama Laravel Echo & Pusher

HTTP Methods: GET, POST, PUT, PATCH, DELETE

Setiap request HTTP memiliki "method" yang menunjukkan jenis aksi yang diminta. Laravel menyediakan helper untuk mendefinisikan route yang merespons method tertentu — dan ini bukan sekadar konvensi, melainkan bagian penting dari arsitektur RESTful yang akan kita pelajari lebih dalam saat membahas CRUD.

routes/web.php
PHP
<?php
// routes/web.php

use Illuminate\Support\Facades\Route;

// GET  → Mengambil/menampilkan data
Route::get('/posts', function () {
    return 'Menampilkan semua post';
});

// POST → Mengirim data baru (form submit)
Route::post('/posts', function () {
    return 'Menyimpan post baru';
});

// PUT  → Update data secara keseluruhan
Route::put('/posts/{id}', function (int $id) {
    return "Update penuh post $id";
});

// PATCH → Update data sebagian
Route::patch('/posts/{id}', function (int $id) {
    return "Update sebagian post $id";
});

// DELETE → Menghapus data
Route::delete('/posts/{id}', function (int $id) {
    return "Hapus post $id";
});

// ANY → Merespons semua HTTP method
Route::any('/webhook', function () {
    return 'Menerima semua method';
});

// MATCH → Merespons method tertentu saja
Route::match(['get', 'post'], '/kontak', function () {
    return 'Hanya GET dan POST';
});

Route Parameter: URL yang Dinamis

Tidak semua URL bersifat statis seperti /tentang atau /kontak. Banyak URL mengandung bagian dinamis — seperti ID pengguna, slug artikel, atau kode produk. Laravel menyediakan dua jenis route parameter untuk menangani ini: required parameter yang wajib ada dan optional parameter yang boleh tidak ada.

#
routes/web.php
PHP
<?php
// routes/web.php

// ── REQUIRED PARAMETER ────────────────────────────────
// {id} wajib ada — akses /posts/1 atau /posts/99
Route::get('/posts/{id}', function (int $id) {
    return "Menampilkan post dengan ID: $id";
});

// Multiple parameters
Route::get('/users/{userId}/posts/{postId}', function (int $userId, int $postId) {
    return "Post $postId milik User $userId";
});
Required Parameter

Parameter wajib didefinisikan dengan kurung kurawal {namaParameter} di dalam URI. Laravel otomatis meng-inject nilai dari URL ke dalam callback atau method controller. Jika parameter tidak ada di URL, Laravel akan mengembalikan 404.

#
routes/web.php
PHP
// ── OPTIONAL PARAMETER ────────────────────────────────
// {kategori?} boleh tidak ada
// /blog         → tampilkan semua post
// /blog/laravel → tampilkan post kategori laravel
Route::get('/blog/{kategori?}', function (?string $kategori = null) {
    if ($kategori) {
        return "Post kategori: $kategori";
    }
    return "Semua post";
});
Optional Parameter

Parameter opsional didefinisikan dengan tanda tanya di dalam kurung kurawal {namaParameter?}. Kamu harus memberikan nilai default di parameter fungsinya karena nilai tersebut bisa null. Berguna untuk fitur seperti filter atau pagination yang tidak selalu dibutuhkan.

#
routes/web.php
PHP
// ── CONSTRAINT dengan where() ─────────────────────────
// Pastikan {id} hanya berisi angka
Route::get('/posts/{id}', function (int $id) {
    return "Post $id";
})->where('id', '[0-9]+');

// Pastikan {slug} hanya berisi huruf, angka, dan strip
Route::get('/artikel/{slug}', function (string $slug) {
    return "Artikel: $slug";
})->where('slug', '[a-z0-9-]+');

// Constraint untuk beberapa parameter sekaligus
Route::get('/users/{id}/posts/{slug}', function (int $id, string $slug) {
    return "Post $slug dari user $id";
})->where([
    'id'   => '[0-9]+',
    'slug' => '[a-z0-9-]+'
]);

// Shorthand constraint yang tersedia
Route::get('/posts/{id}', fn(int $id) => "Post $id")
    ->whereNumber('id');       // Hanya angka

Route::get('/artikel/{slug}', fn(string $slug) => "Artikel: $slug")
    ->whereAlphaNumeric('slug'); // Hanya huruf dan angka

Route::get('/kategori/{nama}', fn(string $nama) => "Kategori: $nama")
    ->whereAlpha('nama');       // Hanya huruf
Constraint dengan Regular Expression

Kamu bisa membatasi format nilai parameter menggunakan metode where() — misalnya memastikan parameter id hanya berisi angka, atau parameter slug hanya berisi huruf dan tanda hubung. Ini membantu mencegah request dengan format yang tidak valid sampai ke controller.


Named Routes: Memberi Nama pada Route

Named routes adalah salah satu fitur Laravel yang paling berguna namun sering diremehkan pemula. Dengan memberi nama pada setiap route, kamu bisa membuat URL di view atau controller berdasarkan nama route — bukan URL string secara langsung. Keuntungannya luar biasa: jika suatu hari URL berubah, kamu cukup mengubah definisi route saja tanpa harus mencari dan mengganti semua URL yang tersebar di seluruh codebase.

routes/web.php
PHP
<?php
// routes/web.php — Mendefinisikan named routes

// Tambahkan ->name('nama.route') di akhir definisi route
Route::get('/posts', [PostController::class, 'index'])
    ->name('posts.index');

Route::get('/posts/{id}', [PostController::class, 'show'])
    ->name('posts.show');

Route::get('/posts/create', [PostController::class, 'create'])
    ->name('posts.create');

Route::post('/posts', [PostController::class, 'store'])
    ->name('posts.store');

Route::get('/posts/{id}/edit', [PostController::class, 'edit'])
    ->name('posts.edit');

Route::put('/posts/{id}', [PostController::class, 'update'])
    ->name('posts.update');

Route::delete('/posts/{id}', [PostController::class, 'destroy'])
    ->name('posts.destroy');
redirect.php
PHP
<?php
// Menggunakan named routes di Controller atau kode PHP lain

// Generate URL berdasarkan nama route
$url = route('posts.index');
// Hasil: http://localhost:8000/posts

// Generate URL dengan parameter
$url = route('posts.show', ['id' => 5]);
// Hasil: http://localhost:8000/posts/5

// Generate URL dengan multiple parameter
$url = route('posts.edit', ['id' => 5]);
// Hasil: http://localhost:8000/posts/5/edit

// Redirect ke named route
return redirect()->route('posts.index');
return redirect()->route('posts.show', ['id' => $post->id]);

// Cek apakah route saat ini adalah route tertentu
if (request()->routeIs('posts.*')) {
    // Aktif di semua route yang dimulai dengan posts.
}
contoh.blade.php
Blade
{{-- Menggunakan named routes di Blade template --}}

{{-- Generate URL di href --}}
<a href="{{ route('posts.index') }}">Semua Post</a>
<a href="{{ route('posts.show', ['id' => $post->id]) }}">{{ $post->title }}</a>
<a href="{{ route('posts.create') }}">Tulis Post Baru</a>
<a href="{{ route('posts.edit', ['id' => $post->id]) }}">Edit</a>

{{-- Form action menggunakan named route --}}
<form action="{{ route('posts.store') }}" method="POST">
    @csrf
    {{-- isi form --}}
</form>

{{-- Form untuk DELETE (HTML form hanya support GET dan POST) --}}
<form action="{{ route('posts.destroy', ['id' => $post->id]) }}" method="POST">
    @csrf
    @method('DELETE')
    <button type="submit">Hapus</button>
</form>

{{-- Tandai link sebagai aktif berdasarkan route saat ini --}}
<a href="{{ route('posts.index') }}"
   class="{{ request()->routeIs('posts.index') ? 'active' : '' }}">
   Posts
</a>

💡 Biasakan selalu memberi nama pada setiap route sejak awal project. Ini adalah salah satu best practice paling penting di Laravel yang akan menghemat banyak waktu saat project berkembang. Jika URL berubah di kemudian hari — misalnya dari /posts menjadi /artikel — kamu hanya perlu mengubah satu baris di routes/web.php, dan seluruh aplikasi otomatis mengikuti.


Route Groups: Mengelompokkan Route

Saat aplikasi berkembang, file routes/web.php bisa memiliki puluhan bahkan ratusan route. Route groups memungkinkan kamu mengelompokkan route-route yang memiliki kesamaan — prefix URL yang sama, middleware yang sama, atau namespace controller yang sama — tanpa harus mendefinisikan ulang kesamaan tersebut untuk setiap route.

#
routes/web.php
PHP
<?php
// routes/web.php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Admin;
use App\Http\Controllers\Post\PostController;

// ── GROUP DENGAN PREFIX ───────────────────────────────
// Semua route di dalam group ini dimulai dengan /admin
Route::prefix('admin')->group(function () {
    Route::get('/', fn() => 'Dashboard Admin');         // /admin
    Route::get('/users', fn() => 'Daftar User');       // /admin/users
    Route::get('/settings', fn() => 'Pengaturan');     // /admin/settings
});
Group dengan Prefix URL

Gunakan prefix() untuk mengelompokkan route yang memiliki bagian URL awal yang sama. Misalnya semua route admin yang dimulai dengan /admin, atau semua route API versioning yang dimulai dengan /v1 — cukup definisikan prefix sekali di group, tidak perlu menulis ulang di setiap route.

#
routes/web.php
PHP
// ── GROUP DENGAN MIDDLEWARE ───────────────────────────
// Semua route membutuhkan user yang sudah login
Route::middleware(['auth'])->group(function () {
    Route::get('/dashboard', fn() => 'Dashboard');
    Route::get('/profile', fn() => 'Profil Saya');
    Route::get('/settings', fn() => 'Pengaturan');
});
Group dengan Middleware

Gunakan middleware() untuk menerapkan satu atau beberapa middleware ke semua route dalam group sekaligus. Contoh paling umum: melindungi semua halaman yang membutuhkan login dengan middleware auth, tanpa harus menambahkan middleware tersebut ke setiap route satu per satu.

#
routes/web.php
PHP
// ── GROUP DENGAN PREFIX + MIDDLEWARE + NAME ───────────
Route::prefix('admin')
    ->middleware(['auth', 'role:admin'])
    ->name('admin.')
    ->group(function () {
        // URL: /admin/posts, Name: admin.posts.index
        Route::get('/posts', [Admin\PostController::class, 'index'])
            ->name('posts.index');

        // URL: /admin/users, Name: admin.users.index
        Route::get('/users', [Admin\UserController::class, 'index'])
            ->name('users.index');

        // URL: /admin/settings, Name: admin.settings
        Route::get('/settings', [Admin\SettingController::class, 'index'])
            ->name('settings');
    });
Group dengan Name Prefix

Kombinasikan prefix URL dengan name prefix agar named routes di dalam group memiliki awalan nama yang konsisten. Ini membuat penamaan route lebih terorganisir dan mencerminkan hierarki URL aplikasimu.

#
routes/web.php
PHP
// ── NESTED GROUP ──────────────────────────────────────
Route::prefix('admin')->middleware('auth')->name('admin.')->group(function () {
    // Route level admin
    Route::get('/', fn() => 'Dashboard')->name('dashboard');

    // Sub-group untuk manajemen post
    Route::prefix('posts')->name('posts.')->group(function () {
        Route::get('/', fn() => 'List Post')->name('index');   // admin.posts.index
        Route::get('/create', fn() => 'Form Post')->name('create'); // admin.posts.create
    });

    // Sub-group dengan middleware tambahan
    Route::prefix('settings')
        ->middleware('role:superadmin')
        ->name('settings.')
        ->group(function () {
            Route::get('/', fn() => 'Settings')->name('index');
        });
});
Nested Group

Group bisa di-nested (dimasukkan satu di dalam yang lain) untuk mengekspresikan hierarki yang lebih kompleks — misalnya grup admin yang di dalamnya ada sub-grup untuk manajemen pengguna, dengan middleware dan prefix masing-masing.


Resource Routes: CRUD dalam Satu Baris

Salah satu fitur paling produktif di Laravel adalah resource routing — mendefinisikan semua route CRUD (Create, Read, Update, Delete) yang dibutuhkan sebuah fitur hanya dengan satu baris kode. Laravel secara otomatis membuat tujuh route standar yang mengikuti konvensi RESTful, lengkap dengan HTTP method yang tepat untuk masing-masing aksi.

routes/web.php
PHP
<?php
// routes/web.php

// Satu baris ini membuat TUJUH route sekaligus!
Route::resource('posts', PostController::class);

// Tujuh route yang dibuat secara otomatis:
// ┌─────────┬──────────────────────┬──────────────────┬───────────────┐
// │ Method   │ URI                   │ Action/Method    │ Route Name    │
// ├─────────┼──────────────────────┼──────────────────┼───────────────┤
// │ GET      │ /posts                │ index()          │ posts.index   │
// │ GET      │ /posts/create         │ create()         │ posts.create  │
// │ POST     │ /posts                │ store()          │ posts.store   │
// │ GET      │ /posts/{post}         │ show()           │ posts.show    │
// │ GET      │ /posts/{post}/edit    │ edit()           │ posts.edit    │
// │ PUT/PATCH│ /posts/{post}         │ update()         │ posts.update  │
// │ DELETE   │ /posts/{post}         │ destroy()        │ posts.destroy │
// └─────────┴──────────────────────┴──────────────────┴───────────────┘

// Hanya buat route tertentu saja
Route::resource('posts', PostController::class)
    ->only(['index', 'show']);

// Kecualikan route tertentu
Route::resource('posts', PostController::class)
    ->except(['create', 'edit']);

// Resource di dalam group
Route::prefix('admin')->name('admin.')->group(function () {
    Route::resource('posts', Admin\PostController::class);
    Route::resource('users', Admin\UserController::class);
    Route::resource('categories', Admin\CategoryController::class);
    // Membuat route admin.posts.index, admin.posts.create, dll
});

// Nested resource (misal: komentar milik post)
Route::resource('posts.comments', CommentController::class);
// Membuat: /posts/{post}/comments
//          /posts/{post}/comments/{comment}
//          dst.

💡 Saat menggunakan Route::resource(), pastikan Controller yang kamu buat menggunakan flag --resource saat membuatnya dengan Artisan: php artisan make:controller PostController --resource --model=Post. Ini otomatis membuat tujuh method (index, create, store, show, edit, update, destroy) yang sesuai dengan tujuh route resource.


Route Model Binding: Parameter Otomatis Jadi Model

Route Model Binding adalah fitur yang sangat elegan di Laravel — ia secara otomatis mengambil record dari database berdasarkan parameter di URL dan meng-inject-nya langsung ke dalam Controller sebagai instance Model. Tidak perlu lagi menulis Post::findOrFail($id) secara manual di setiap method Controller.

#
routes/web.php
PHP
<?php
// ── IMPLICIT BINDING ──────────────────────────────────

// routes/web.php
Route::get('/posts/{post}', [PostController::class, 'show']);
//                   ↑ nama parameter harus sama dengan nama variabel di Controller

// app/Http/Controllers/PostController.php
public function show(Post $post)
//                   ↑ Laravel otomatis cari Post::findOrFail($id)
//                     dari nilai {post} di URL
{
    // $post sudah berisi instance Model Post yang sesuai
    // Jika tidak ditemukan → otomatis 404
    return view('posts.show', compact('post'));
}

// Contoh akses:
// GET /posts/5 → $post = Post::findOrFail(5)
// GET /posts/99 (tidak ada) → otomatis 404
Implicit Binding: Otomatis Berdasarkan Nama Parameter

Jika nama parameter route sesuai dengan nama variabel di method Controller dan tipe datanya adalah Model, Laravel otomatis mengambil record dari database. Jika record tidak ditemukan, Laravel otomatis mengembalikan response 404 tanpa perlu kode tambahan.

#
routes/web.php
PHP
// ── CUSTOM KEY BINDING ────────────────────────────────
// Cari post berdasarkan slug, bukan id
Route::get('/posts/{post:slug}', [PostController::class, 'show']);

// GET /posts/belajar-laravel-routing
// → $post = Post::where('slug', 'belajar-laravel-routing')->firstOrFail()

public function show(Post $post)
{
    // $post adalah instance Post dengan slug yang sesuai
    return view('posts.show', compact('post'));
}
Custom Key: Cari Berdasarkan Kolom Selain ID

Secara default Laravel mencari berdasarkan kolom id. Jika ingin mencari berdasarkan kolom lain seperti slug, kamu bisa menentukan kolom pencarian langsung di definisi route menggunakan sintaks {post:slug}.

#
routes/web.php
PHP
// ── MULTIPLE MODEL BINDING ────────────────────────────
// Nested resource dengan dua model binding
Route::get('/users/{user}/posts/{post}', [PostController::class, 'userPost']);

public function userPost(User $user, Post $post)
{
    // $user dan $post keduanya otomatis di-resolve
    // Laravel juga memastikan $post milik $user (scoped binding)
    return view('posts.user-post', compact('user', 'post'));
}
Explicit Binding: Konfigurasi Manual

Untuk kebutuhan yang lebih kompleks — misalnya custom query dengan eager loading atau scope tertentu — kamu bisa mendefinisikan binding secara eksplisit di RouteServiceProvider menggunakan metode Route::bind().


Redirect Routes dan View Routes

Tidak semua route harus melewati Controller. Laravel menyediakan shortcut untuk kasus umum yang sering muncul: redirect dari satu URL ke URL lain, dan menampilkan view langsung dari route tanpa perlu membuat Controller khusus.

routes/web.php
PHP
<?php
// routes/web.php

// ── REDIRECT ROUTES ───────────────────────────────────

// Redirect permanen (301) dari URL lama ke URL baru
Route::redirect('/home', '/dashboard');

// Redirect dengan status code spesifik
Route::redirect('/lama', '/baru', 301);  // Permanent redirect
Route::redirect('/temp', '/baru', 302);  // Temporary redirect

// Redirect permanen (shorthand)
Route::permanentRedirect('/home', '/dashboard');

// ── VIEW ROUTES ───────────────────────────────────────

// Tampilkan view langsung tanpa Controller
// Berguna untuk halaman statis seperti about, contact, privacy policy
Route::view('/tentang', 'pages.tentang');

// Dengan data yang dikirim ke view
Route::view('/tentang', 'pages.tentang', [
    'judul'   => 'Tentang Kami',
    'company' => 'DevPath Indonesia',
    'tahun'   => date('Y'),
]);

Route::view('/kebijakan-privasi', 'pages.privacy');
Route::view('/syarat-ketentuan', 'pages.terms');
Route::view('/kontak', 'pages.kontak');

Fallback Route: Menangani URL yang Tidak Dikenal

Apa yang terjadi saat pengguna mengakses URL yang tidak terdaftar di routes/web.php? Secara default Laravel akan menampilkan halaman 404 bawaan. Dengan fallback route, kamu bisa mengkustomisasi penanganan URL yang tidak dikenal — menampilkan halaman 404 yang sesuai dengan desain aplikasimu atau melakukan aksi lain yang diperlukan.

routes/web.php
PHP
<?php
// routes/web.php

// Semua route lain harus didefinisikan SEBELUM fallback route
Route::get('/', fn() => view('welcome'));
Route::resource('posts', PostController::class);
// ... route lainnya ...

// Fallback route — selalu taruh paling bawah!
// Akan dieksekusi jika tidak ada route lain yang cocok
Route::fallback(function () {
    return response()->view('errors.404', [], 404);
    // Atau redirect ke halaman utama:
    // return redirect('/');
});

Melihat Semua Route dan Debugging

Saat project berkembang dan memiliki puluhan route, kamu perlu cara cepat untuk memverifikasi bahwa semua route terdaftar dengan benar, melihat middleware apa yang diterapkan, dan menemukan route tertentu dengan cepat. Artisan menyediakan perintah route:list yang sangat berguna untuk ini.

route-list.txt
Text
# Tampilkan semua route yang terdaftar
php artisan route:list

# Output contoh:
#  GET|HEAD   /              ................................ welcome
#  GET|HEAD   /posts         PostController@index ......... posts.index
#  POST       /posts         PostController@store ......... posts.store
#  GET|HEAD   /posts/create  PostController@create ....... posts.create
#  GET|HEAD   /posts/{post}  PostController@show ......... posts.show
#  PUT|PATCH  /posts/{post}  PostController@update ....... posts.update
#  DELETE     /posts/{post}  PostController@destroy ...... posts.destroy
#  GET|HEAD   /posts/{post}/edit PostController@edit .... posts.edit

# Filter berdasarkan path/URI
php artisan route:list --path=admin

# Filter berdasarkan nama route
php artisan route:list --name=posts

# Tampilkan dengan detail middleware
php artisan route:list -v

# Tampilkan hanya route yang menggunakan middleware tertentu
php artisan route:list --middleware=auth

# Format output sebagai JSON (berguna untuk tooling)
php artisan route:list --json

Praktik: Membuat Routing Lengkap untuk Blog Sederhana

Mari aplikasikan semua konsep routing yang sudah dipelajari dalam satu contoh komprehensif — mendefinisikan routing lengkap untuk aplikasi blog sederhana dengan halaman publik, panel admin yang dilindungi autentikasi, dan API endpoint. Ini mensimulasikan struktur routing yang akan kamu temui di proyek nyata.

routes/web.php
PHP
<?php
// routes/web.php — Routing lengkap untuk aplikasi blog

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\{
    HomeController,
    PostController,
    CategoryController,
    PageController,
};
use App\Http\Controllers\Admin;

// ── HALAMAN PUBLIK ────────────────────────────────────

// Halaman utama
Route::get('/', [HomeController::class, 'index'])->name('home');

// Halaman statis
Route::view('/tentang', 'pages.tentang')->name('about');
Route::view('/kontak', 'pages.kontak')->name('contact');

// Blog publik
Route::prefix('blog')->name('blog.')->group(function () {
    // /blog → semua artikel
    Route::get('/', [PostController::class, 'index'])
        ->name('index');

    // /blog/laravel → artikel per kategori
    Route::get('/kategori/{category:slug}', [CategoryController::class, 'show'])
        ->name('category');

    // /blog/belajar-laravel-routing → detail artikel
    Route::get('/{post:slug}', [PostController::class, 'show'])
        ->name('show');
});

// ── AUTENTIKASI ───────────────────────────────────────
Route::redirect('/home', '/dashboard');

// ── AREA YANG BUTUH LOGIN ─────────────────────────────
Route::middleware(['auth'])->group(function () {
    Route::get('/dashboard', fn() => view('dashboard'))
        ->name('dashboard');

    Route::get('/profile', fn() => view('profile'))
        ->name('profile');
});

// ── PANEL ADMIN ───────────────────────────────────────
Route::prefix('admin')
    ->middleware(['auth', 'role:admin'])
    ->name('admin.')
    ->group(function () {
        // Dashboard admin
        Route::get('/', [Admin\DashboardController::class, 'index'])
            ->name('dashboard');

        // Kelola post, kategori, komentar
        Route::resource('posts', Admin\PostController::class);
        Route::resource('categories', Admin\CategoryController::class);
        Route::resource('users', Admin\UserController::class)
            ->except(['create', 'store']);

        // Route kustom di luar resource
        Route::patch('/posts/{post}/publish', [Admin\PostController::class, 'publish'])
            ->name('posts.publish');

        Route::patch('/posts/{post}/unpublish', [Admin\PostController::class, 'unpublish'])
            ->name('posts.unpublish');
    });

// ── FALLBACK ──────────────────────────────────────────
Route::fallback(fn() => response()->view('errors.404', [], 404));

💡 Perhatikan urutan definisi route di atas — route yang lebih spesifik selalu didefinisikan sebelum yang lebih umum. Misalnya /blog/kategori/{category:slug} didefinisikan sebelum /blog/{post:slug}, agar Laravel tidak salah mencocokkan URL /blog/kategori/laravel sebagai slug post. Urutan penulisan route sangat berpengaruh pada cara Laravel mencocokkan URL.


Langkah Selanjutnya

Selamat — kamu sudah menguasai sistem routing Laravel secara menyeluruh! Mulai dari route dasar, HTTP methods, parameter dinamis, named routes, route groups, resource routing, route model binding, hingga membangun struktur routing lengkap untuk aplikasi nyata. Di bagian ketiga seri ini, kita akan menyelami Controller secara mendalam — cara membuat Controller yang terorganisir, memahami tujuh method resource controller, dan menerapkan prinsip Single Responsibility agar kode tetap bersih dan mudah di-maintain.

🚀 Sebelum lanjut ke Bagian 3, pastikan kamu sudah berlatih: (1) membuat berbagai jenis route di routes/web.php projectmu, (2) menggunakan named routes di Blade template dengan helper route(), (3) membuat route group dengan prefix dan middleware, (4) menggunakan Route::resource() dan melihat hasilnya dengan php artisan route:list. Semakin banyak kamu bereksperimen dengan routing, semakin intuitif konsep ini terasa saat membangun fitur nyata.