Breadcrumbs
Breadcrumb adalah jejak navigasi di atas heading page. Jejak ini dibangun di server dan dikirim sebagai bagian dari metadata page. Setiap screen Panel otomatis memilikinya: standalone page dimulai dari dashboard lalu melewati navigation group-nya, sedangkan resource page berjalan dari dashboard menuju resource index lalu ke record. Gunakan API di bawah ketika breadcrumb sebuah page perlu menjelaskan sesuatu yang tidak dapat ditentukan oleh default.
Contoh minimal yang berfungsi
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Pages;
use PandaPanel\Pages\Page;
use PandaPanel\Support\Breadcrumb;
final class Throughput extends Page
{
protected static ?string $title = 'Throughput';
/**
* @return list<Breadcrumb>
*/
public function breadcrumbs(): array
{
return [
Breadcrumb::make('Dashboard')->url($this->dashboardUrl()),
Breadcrumb::make('Reports')->url('/admin/reports'),
Breadcrumb::make('Throughput')->current(),
];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Header sekarang menampilkan Dashboard / Reports / Throughput; dua item pertama berupa link, sedangkan item terakhir berupa teks biasa.
Class Breadcrumb
PandaPanel\Support\Breadcrumb adalah final readonly value object. Setiap method mengembalikan instance baru.
public function __construct(
public string $label,
public ?string $href = null,
public bool $current = false,
);
public static function make(string $label): self;
public function url(?string $href): self;
public function current(bool $current = true): self;
/** @return array{label: string, href: string|null, current: bool} */
public function toArray(): array;2
3
4
5
6
7
8
9
10
11
12
| Pemanggilan | Hasil |
|---|---|
Breadcrumb::make('Users') | teks biasa, bukan link, dan bukan current |
Breadcrumb::make('Users')->url('/admin/users') | sebuah link |
Breadcrumb::make('Users')->current() | page yang sedang dilihat |
Breadcrumb::make('Users')->url('/admin/users')->current() | memiliki link dan ditandai current |
use PandaPanel\Support\Breadcrumb;
Breadcrumb::make('Users')->url('/admin/users')->toArray();
// ['label' => 'Users', 'href' => '/admin/users', 'current' => false]
Breadcrumb::make('Ada Lovelace')->current()->toArray();
// ['label' => 'Ada Lovelace', 'href' => null, 'current' => true]
Breadcrumb::make('Users')->url(null); // drops the link again2
3
4
5
6
7
8
9
Label selalu plain text. Vue merendernya sebagai teks, sehingga label yang berisi markup akan menampilkan markup tersebut sebagai teks biasa.
Breadcrumb default pada standalone page
/** @return list<Breadcrumb> */
public function breadcrumbs(): array;2
PandaPanel\Pages\Page::breadcrumbs() membangun tiga bagian; bagian tengah hanya ada ketika page memiliki navigation group:
$crumbs = [Breadcrumb::make('Dashboard')->url($this->dashboardUrl())];
$group = NavigationGroupName::resolve(static::$navigationGroup);
if ($group !== null) {
$crumbs[] = Breadcrumb::make($group);
}
$crumbs[] = Breadcrumb::make(static::title())->current();2
3
4
5
6
7
8
9
Karena itu contoh page Admin Settings yang mendeklarasikan $navigationGroup = 'System' akan mengirim:
[
['label' => 'Dashboard', 'href' => '/admin', 'current' => false],
['label' => 'System', 'href' => null, 'current' => false],
['label' => 'Settings', 'href' => null, 'current' => true],
]2
3
4
5
Breadcrumb group sengaja tidak memiliki href: navigation group adalah heading pada sidebar, bukan page, sehingga tidak ada tujuan untuk dibuka. Nama group di-resolve melalui PandaPanel\Support\NavigationGroupName, sehingga group berbasis enum menggunakan value enum tersebut, atau name case untuk pure enum.
dashboardUrl() menggunakan route($this->panel()->routeName('dashboard'), absolute: false). Karena itu breadcrumb pertama menunjuk ke root Panel, bukan ke /.
Dashboard sendiri meng-override seluruh breadcrumb karena dashboard adalah root:
public function breadcrumbs(): array
{
return [Breadcrumb::make('Dashboard')->current()];
}2
3
4
Breadcrumb default pada resource page
PandaPanel\Resources\Pages\ResourcePage menyusun breadcrumb melalui helper, bukan membangunnya inline, sehingga setiap page pada sebuah Resource menggunakan bagian awal breadcrumb yang konsisten.
/** @return list<Breadcrumb> */
protected function baseBreadcrumbs(): array;
/** @return list<Breadcrumb> */
protected function parentBreadcrumbs(): array;
protected function recordCrumb(Model $record, string $title): Breadcrumb;
/** @return list<array{label: string, href: string|null, current: bool}> */
protected function serializeBreadcrumbs(array $crumbs): array;2
3
4
5
6
7
8
9
10
baseBreadcrumbs() adalah dashboard → parent trail → resource index:
return [
Breadcrumb::make('Dashboard')->url($this->dashboardUrl()),
...$this->parentBreadcrumbs(),
Breadcrumb::make($resource::pluralLabel())->url($resource::url()),
];2
3
4
5
Setiap page kemudian menambahkan breadcrumb terakhir miliknya sendiri:
| Page | Breadcrumb terakhir |
|---|---|
ListRecords | Breadcrumb::make($resource::pluralLabel())->current() — tanpa base karena index itu sendiri adalah base |
CreateRecord | Breadcrumb::make('New')->current() |
ViewRecord | Breadcrumb::make($recordTitle)->current() |
EditRecord | breadcrumb record, lalu Breadcrumb::make('Edit')->current() |
ManageRelatedRecords | breadcrumb record, lalu Breadcrumb::make($manager::title())->current() |
parentBreadcrumbs() mengembalikan [] untuk Resource yang tidak nested. Dengan begitu trail dapat dibangun dengan pola yang sama untuk semua page. Untuk nested Resource, method tersebut menambahkan dua breadcrumb: plural label parent yang mengarah ke index serta parent record:
Breadcrumb::make($parentResource::pluralLabel())->url($parentResource::url()),
$canView
? Breadcrumb::make($title)->url($parentResource::url('view', $parent))
: Breadcrumb::make($title),2
3
4
recordCrumb() mengikuti rule yang sama untuk record saat ini: menjadi link menuju view page hanya jika Resource memang memiliki view page dan canView() mengizinkannya; jika tidak, hanya plain text. Breadcrumb yang jika diklik akan menghasilkan 403 tidak pernah dirender sebagai link.
Membuat breadcrumb custom pada resource page
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Resources\Pages\ResourcePage;
use PandaPanel\Support\Breadcrumb;
/**
* @return array<string, mixed>
*/
protected function pageMetadata(Model $record): array
{
return [
...$this->headingMetadata($record),
'breadcrumbs' => $this->serializeBreadcrumbs([
...$this->baseBreadcrumbs(),
$this->recordCrumb($record, $this->recordTitle($record)),
Breadcrumb::make('Audit')->current(),
]),
'headerActions' => [],
'scope' => static::renderHookScope(),
'cluster' => $this->clusterNavigation(),
'subNavigation' => $this->subNavigation($record, 'audit'),
];
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
serializeBreadcrumbs() adalah satu-satunya tahap yang mengubah object menjadi array. Standalone Page melakukan serialisasi tersebut di dalam metadata(), sehingga breadcrumbs() pada standalone page tetap mengembalikan object.
Menonaktifkan breadcrumb
$panel->breadcrumbs(false);
public function hasBreadcrumbs(): bool;2
Konfigurasi ini menghilangkan trail dari shell, bukan sekadar menyembunyikannya. Kiosk atau Panel dengan satu page mungkin tidak membutuhkan breadcrumb. Page tetap menghitung breadcrumb-nya; hanya topbar yang tidak menggambarkannya.
Lihat Sidebar and header layouts.
Di frontend
export interface PanelBreadcrumbItem {
label: string;
href: string | null;
current: boolean;
}2
3
4
5
Secara default PanelLayout mengambil trail dari metadata page, sehingga page tidak perlu melakukan wiring apa pun. Explicit prop akan menang, berguna untuk page langka yang membangun breadcrumb di sisi client:
<script setup lang="ts">
import PanelLayout from '@/panel/layouts/PanelLayout.vue';
import type { PanelBreadcrumbItem } from '@/panel/types/breadcrumb';
const breadcrumbs: PanelBreadcrumbItem[] = [
{ label: 'Dashboard', href: '/admin', current: false },
{ label: 'Live', href: null, current: true },
];
</script>
<template>
<PanelLayout :breadcrumbs="breadcrumbs">
<slot />
</PanelLayout>
</template>2
3
4
5
6
7
8
9
10
11
12
13
14
15
PanelBreadcrumb.vue merender setiap item sebagai BreadcrumbLink yang membungkus Inertia <Link>, kecuali item dengan current === true atau href === null. Kedua kondisi tersebut dirender sebagai BreadcrumbPage berupa plain text. Separator hanya diletakkan di antara item, tidak setelah item terakhir. Array kosong tidak merender apa pun, bukan bar kosong.
normalizePageMetadata() memvalidasi setiap breadcrumb ketika data melewati boundary: item tanpa label string dibuang, sedangkan href yang bukan string berubah menjadi null. Trail yang malformed akan terdegradasi menjadi lebih pendek daripada melempar error di dalam layout.
Gotchas
- Breadcrumb berupa object pada
Pagedan array padaResourcePage.Page::breadcrumbs()mengembalikanlist<Breadcrumb>danmetadata()melakukan serialisasi; resource page melakukan serialisasi di dalampageMetadata()menggunakanserializeBreadcrumbs(). Mengembalikan array dariPage::breadcrumbs()akan gagal ketikatoArray()dipanggil terhadap item tersebut. current()tidak berarti item tersebut harus berada di posisi terakhir. Ini hanya flag, bukan index. Menandai dua breadcrumb sebagai current akan merender dua breadcrumb plain-text.- Breadcrumb tanpa
hrefdirender sebagai teks walaupun tidakcurrent. Inilah bentuk breadcrumb navigation group dan merupakan shape yang benar untuk langkah yang memang bukan page. - Label tidak pernah berupa HTML. Menyisipkan nama record aman; menyisipkan markup tidak.
dashboardUrl()membutuhkan current Panel. Page yang diinstansiasi di luar request Panel melemparPanelRegistrationException::noCurrentPanel()daripanel()sebelum breadcrumb dapat dibangun.- Mematikan breadcrumb tidak menghemat proses perhitungan. Breadcrumb tetap dihitung dan tetap dikirim; yang berhenti hanya rendering.