Dalam aplikasi Laravel yang menggunakan Inertia.js + Vue 3, kebutuhan layout biasanya berkembang cukup cepat. Pada awal proyek, satu layout utama mungkin sudah cukup. Namun ketika aplikasi mulai memiliki area publik, area autentikasi, dashboard, modul admin, dan halaman dengan kebutuhan navigasi berbeda, pendekatan layout tunggal akan mulai terasa kaku. Di titik inilah layout bertingkat menjadi penting.
Artikel ini membahas cara menyusun layout bertingkat yang tetap sederhana, tetapi cukup fleksibel untuk proyek skala menengah. Kita akan membahas struktur Main Layout, Dashboard Layout, halaman publik, sidebar aktif berdasarkan route, breadcrumb, title dinamis, reusable components, dan pola organisasi file agar aplikasi tetap modular saat jumlah halaman bertambah.
Mengapa Layout Bertingkat Penting?
Pada Inertia.js, halaman Vue berfungsi seperti halaman server-side, tetapi dirender dengan komponen frontend. Karena itu, layout bukan hanya urusan tampilan. Layout juga menjadi tempat yang baik untuk menaruh:
- struktur navigasi global,
- header dan sidebar,
- metadata halaman seperti title,
- breadcrumb,
- slot untuk action per halaman,
- state UI yang dipakai lintas halaman.
Jika semua kebutuhan tersebut diletakkan langsung di tiap halaman, kode akan cepat berulang dan sulit dirawat. Dengan layout bertingkat, kita bisa memisahkan tanggung jawab seperti berikut:
- AppLayout: kerangka paling atas, misalnya wrapper global, flash message, title default.
- DashboardLayout: khusus area aplikasi internal dengan sidebar, topbar, breadcrumb.
- PublicLayout: untuk landing page, halaman profil publik, dokumentasi, atau pricing.
Pola ini bekerja baik karena setiap lapisan layout menangani konteks yang berbeda. Halaman tidak perlu tahu bagaimana sidebar dirender; halaman hanya perlu mengirim data yang diperlukan, misalnya title atau breadcrumb.
Struktur Folder yang Modular untuk Proyek Skala Menengah
Salah satu kesalahan umum adalah mencampur semua halaman, layout, dan komponen ke dalam satu folder besar. Pada awalnya terlihat sederhana, tetapi saat jumlah modul bertambah, struktur ini menyulitkan pencarian file dan meningkatkan risiko duplikasi.
Struktur berikut cukup seimbang untuk proyek skala menengah:
resources/js/
├── Components/
│ ├── Breadcrumbs.vue
│ ├── NavLink.vue
│ ├── PageHeader.vue
│ └── SidebarMenu.vue
├── Layouts/
│ ├── AppLayout.vue
│ ├── DashboardLayout.vue
│ └── PublicLayout.vue
├── Pages/
│ ├── Public/
│ │ ├── Home.vue
│ │ └── About.vue
│ ├── Dashboard/
│ │ ├── Home.vue
│ │ ├── Reports/
│ │ │ └── Index.vue
│ │ └── Users/
│ │ ├── Index.vue
│ │ └── Show.vue
│ └── Auth/
│ └── Login.vue
├── Composables/
│ ├── useBreadcrumbs.js
│ └── usePageMeta.js
└── app.jsBeberapa catatan praktis:
- Layouts dipisahkan dari Components karena tanggung jawabnya berbeda.
- Pages dikelompokkan berdasarkan area aplikasi, bukan hanya berdasarkan tipe file.
- Composables menampung logika yang bisa dipakai ulang seperti pembentukan breadcrumb atau title.
Jika aplikasi sangat modular, Anda juga bisa mengelompokkan per domain, misalnya Modules/Users, Modules/Reports, masing-masing berisi pages dan komponen lokalnya. Namun untuk proyek skala menengah, struktur di atas biasanya lebih mudah diadopsi tim tanpa menambah kompleksitas berlebihan.
Membangun Layout Utama dan Layout Bertingkat
AppLayout sebagai Fondasi Global
AppLayout adalah lapisan terluar. Ia cocok untuk menaruh elemen global seperti notifikasi, wrapper container, atau title default. Tujuannya bukan untuk memuat semua navigasi, tetapi menyediakan fondasi yang konsisten.
<!-- resources/js/Layouts/AppLayout.vue -->
<script setup>
import { Head, usePage } from '@inertiajs/vue3'
import { computed } from 'vue'
const props = defineProps({
title: {
type: String,
default: 'Aplikasi'
}
})
const page = usePage()
const appName = computed(() => page.props.appName || 'Aplikasi')
const fullTitle = computed(() => `${props.title} - ${appName.value}`)
</script>
<template>
<Head :title="fullTitle" />
<div class="min-h-screen bg-gray-50 text-gray-900">
<slot />
</div>
</template>Di sini, Head dari Inertia dipakai untuk mengatur title halaman secara dinamis. Ini lebih baik daripada memanipulasi document.title manual karena tetap konsisten dengan mekanisme Inertia.
PublicLayout untuk Halaman Publik
Halaman publik biasanya memiliki header sederhana, footer, dan tanpa sidebar aplikasi internal.
<!-- resources/js/Layouts/PublicLayout.vue -->
<script setup>
import AppLayout from './AppLayout.vue'
defineProps({
title: String
})
</script>
<template>
<AppLayout :title="title">
<header class="border-b bg-white">
<div class="mx-auto max-w-6xl px-4 py-4">
Situs Publik
</div>
</header>
<main class="mx-auto max-w-6xl px-4 py-8">
<slot />
</main>
<footer class="border-t bg-white">
<div class="mx-auto max-w-6xl px-4 py-4 text-sm text-gray-500">
© 2026
</div>
</footer>
</AppLayout>
</template>DashboardLayout untuk Area Internal
Untuk area dashboard, kita biasanya memerlukan sidebar, topbar, breadcrumb, dan area konten utama. Layout ini mewarisi AppLayout agar logika global tetap terpusat.
<!-- resources/js/Layouts/DashboardLayout.vue -->
<script setup>
import AppLayout from './AppLayout.vue'
import SidebarMenu from '@/Components/SidebarMenu.vue'
import Breadcrumbs from '@/Components/Breadcrumbs.vue'
import PageHeader from '@/Components/PageHeader.vue'
defineProps({
title: String,
breadcrumbs: {
type: Array,
default: () => []
}
})
</script>
<template>
<AppLayout :title="title">
<div class="flex min-h-screen">
<aside class="w-64 border-r bg-white">
<SidebarMenu />
</aside>
<div class="flex-1">
<header class="border-b bg-white px-6 py-4">
<PageHeader :title="title">
<template #breadcrumb>
<Breadcrumbs :items="breadcrumbs" />
</template>
<template #actions>
<slot name="actions" />
</template>
</PageHeader>
</header>
<main class="p-6">
<slot />
</main>
</div>
</div>
</AppLayout>
</template>Pendekatan ini memudahkan kita menambahkan action per halaman, misalnya tombol “Tambah User”, tanpa menulis ulang struktur header di setiap page.
Menghubungkan Halaman dengan Layout
Di Vue 3 dengan Inertia, sebuah halaman bisa mendeklarasikan layout-nya sendiri. Ini berguna karena halaman publik dan halaman dashboard tidak harus berbagi wrapper yang sama.
<!-- resources/js/Pages/Dashboard/Users/Index.vue -->
<script>
import DashboardLayout from '@/Layouts/DashboardLayout.vue'
export default {
layout: (h, page) =>
h(DashboardLayout, {
title: 'Daftar Pengguna',
breadcrumbs: [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Pengguna', href: '/dashboard/users' }
]
}, () => page)
}
</script>
<script setup>
import { Link } from '@inertiajs/vue3'
defineProps({
users: Array
})
</script>
<template>
<div>
<ul>
<li v-for="user in users" :key="user.id">
<Link :href="`/dashboard/users/${user.id}`">{{ user.name }}</Link>
</li>
</ul>
</div>
</template>Alternatifnya, Anda bisa menetapkan layout secara global berdasarkan namespace halaman di app.js. Cara ini berguna jika mayoritas halaman pada folder tertentu selalu memakai layout yang sama. Namun tetap sediakan opsi override di level halaman untuk kasus khusus.
Sidebar Aktif Berdasarkan Route
Salah satu kebutuhan paling umum di dashboard adalah menandai menu aktif. Kesalahan yang sering terjadi adalah membandingkan URL mentah secara kaku, misalnya window.location.pathname === '/dashboard/users'. Pendekatan ini rapuh ketika ada query string, nested route, atau parameter dinamis.
Jika Anda menggunakan Ziggy, pendekatan berbasis nama route biasanya lebih stabil. Contohnya:
<!-- resources/js/Components/SidebarMenu.vue -->
<script setup>
import NavLink from './NavLink.vue'
const items = [
{ label: 'Dashboard', route: 'dashboard.home' },
{ label: 'Pengguna', route: 'dashboard.users.index' },
{ label: 'Laporan', route: 'dashboard.reports.index' }
]
</script>
<template>
<nav class="p-4 space-y-1">
<NavLink
v-for="item in items"
:key="item.route"
:href="route(item.route)"
:active="route().current(item.route) || route().current(item.route.replace('.index', '.*'))"
>
{{ item.label }}
</NavLink>
</nav>
</template>Untuk komponen link yang bisa dipakai ulang:
<!-- resources/js/Components/NavLink.vue -->
<script setup>
import { Link } from '@inertiajs/vue3'
defineProps({
href: String,
active: Boolean
})
</script>
<template>
<Link
:href="href"
class="block rounded px-3 py-2 text-sm"
:class="active ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-700 hover:bg-gray-100'"
>
<slot />
</Link>
</template>Menggunakan nama route lebih aman dibanding string URL mentah karena perubahan pola URL di backend tidak selalu memaksa perubahan besar di frontend, selama nama route tetap konsisten.
Breadcrumb dan Title Dinamis yang Konsisten
Breadcrumb sebagai Data, Bukan HTML Hardcoded
Breadcrumb sebaiknya diperlakukan sebagai data terstruktur, bukan markup yang ditulis ulang di setiap halaman. Dengan begitu, komponen breadcrumb tetap reusable dan mudah diubah styling-nya secara terpusat.
<!-- resources/js/Components/Breadcrumbs.vue -->
<script setup>
defineProps({
items: {
type: Array,
default: () => []
}
})
</script>
<template>
<nav v-if="items.length" class="mb-2 text-sm text-gray-500">
<ol class="flex flex-wrap items-center gap-2">
<li v-for="(item, index) in items" :key="index" class="flex items-center gap-2">
<a v-if="item.href && index !== items.length - 1" :href="item.href" class="hover:underline">
{{ item.label }}
</a>
<span v-else>{{ item.label }}</span>
<span v-if="index !== items.length - 1">/</span>
</li>
</ol>
</nav>
</template>Untuk halaman dengan data dinamis, misalnya detail user, breadcrumb bisa dibentuk dari props server:
breadcrumbs: [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Pengguna', href: '/dashboard/users' },
{ label: user.name }
]Title Dinamis yang Tidak Berulang
Title halaman sering diabaikan, padahal penting untuk UX dan SEO pada halaman publik. Dengan menaruh logika title di AppLayout, setiap halaman cukup mengirim title singkat. Layout akan menggabungkannya dengan nama aplikasi.
Jika Anda ingin aturan title lebih fleksibel, buat composable seperti usePageMeta agar pembentukan title, subtitle, dan metadata lain tetap seragam.
Reusable Components untuk Mengurangi Duplikasi
Selain layout, proyek skala menengah biasanya membutuhkan komponen yang dipakai berulang di banyak halaman. Jangan menunggu sampai duplikasi terlalu besar. Beberapa komponen yang hampir selalu berguna:
- PageHeader untuk judul, breadcrumb, dan action.
- NavLink untuk item navigasi dengan state aktif.
- EmptyState untuk daftar yang kosong.
- DataTable wrapper jika banyak halaman listing.
- FormSection untuk halaman create/edit.
Contoh PageHeader sederhana:
<!-- resources/js/Components/PageHeader.vue -->
<script setup>
defineProps({
title: String
})
</script>
<template>
<div class="flex items-start justify-between gap-4">
<div>
<slot name="breadcrumb" />
<h1 class="text-xl font-semibold text-gray-900">{{ title }}</h1>
</div>
<div>
<slot name="actions" />
</div>
</div>
</template>Dengan pola ini, halaman listing user dan listing laporan bisa berbagi struktur header yang sama, hanya berbeda isi slot-nya.
Integrasi dengan Laravel dan Inertia Shared Props
Supaya layout tidak perlu menerima data global dari setiap controller, gunakan shared props dari Inertia. Misalnya untuk nama aplikasi, user login, atau menu tertentu.
// app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'appName' => config('app.name'),
'auth' => [
'user' => $request->user(),
],
]);
}Ini membantu menjaga controller tetap fokus pada data halaman, bukan data global layout. Namun jangan memasukkan terlalu banyak data ke shared props. Jika isinya besar dan tidak selalu dipakai, setiap request Inertia akan membawa payload yang tidak perlu.
Tip: shared props cocok untuk data global kecil dan sering dipakai. Untuk data spesifik halaman atau modul, kirim langsung dari controller yang relevan.
Menjaga Aplikasi Tetap Modular Saat Halaman Bertambah
Saat jumlah halaman mulai banyak, masalah utamanya bukan hanya layout, tetapi batas tanggung jawab. Beberapa praktik berikut sangat membantu:
- Jangan jadikan layout sebagai tempat semua logika. Layout sebaiknya mengatur struktur dan UI lintas halaman, bukan memuat request data kompleks.
- Kelompokkan halaman per area atau domain. Folder Dashboard/Users lebih mudah dipahami daripada semua halaman ditaruh datar dalam satu direktori.
- Ekstrak komponen lokal bila hanya dipakai dalam satu modul. Tidak semua komponen harus masuk folder global Components.
- Gunakan nama route yang konsisten. Ini penting untuk sidebar aktif, breadcrumb, dan maintainability.
- Standarkan kontrak layout. Misalnya semua halaman dashboard menerima
title,breadcrumbs, dan slotactions.
Pada skala menengah, konsistensi seperti ini jauh lebih penting daripada abstraksi yang terlalu pintar. Pola yang sedikit repetitif tetapi jelas biasanya lebih mudah dipelihara tim daripada sistem otomatis yang sulit ditelusuri.
Kesalahan Umum dan Tips Debugging
Layout Tidak Muncul atau Ganda
Biasanya terjadi karena halaman sudah membungkus dirinya dengan layout di template, lalu juga mendeklarasikan properti layout. Pilih satu pendekatan dan gunakan secara konsisten. Untuk Inertia, deklarasi layout di komponen halaman umumnya lebih rapi.
Sidebar Aktif Salah
Periksa apakah Anda membandingkan route name yang tepat. Jika memakai nested route seperti dashboard.users.show, menu “Pengguna” mungkin perlu aktif untuk semua turunan dashboard.users.*, bukan hanya dashboard.users.index.
Breadcrumb Tidak Update
Jika breadcrumb dibangun dari props yang berubah setelah navigasi, pastikan datanya memang dikirim ulang oleh response Inertia. Gunakan Vue DevTools atau inspeksi object usePage().props untuk memastikan nilainya sesuai ekspektasi.
Title Halaman Tidak Sesuai
Pastikan tidak ada komponen lain yang juga mengatur Head dengan title berbeda. Secara umum, simpan aturan title di layout atau composable terpusat agar tidak saling bertabrakan.
Penutup
Layout bertingkat pada Inertia.js + Vue 3 di Laravel membantu menjaga aplikasi tetap rapi ketika area publik dan area dashboard mulai berkembang. Dengan memisahkan AppLayout, PublicLayout, dan DashboardLayout, Anda bisa mengelola title dinamis, breadcrumb, sidebar aktif, serta komponen reusable secara lebih konsisten.
Untuk proyek skala menengah, kunci utamanya adalah struktur folder yang jelas, kontrak layout yang konsisten, dan pemisahan tanggung jawab yang disiplin. Hindari menaruh terlalu banyak logika di halaman atau layout. Gunakan reusable components dan shared props seperlunya. Dengan begitu, penambahan halaman baru tidak membuat struktur aplikasi cepat berantakan.
Jika pola ini diterapkan sejak awal, tim akan lebih mudah mengembangkan modul baru, melakukan refactor, dan menjaga pengalaman pengguna tetap konsisten di seluruh aplikasi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!