Skip to content

glhd/gretel

Repository files navigation

Gretel from the story 'Hansel and Gretel' holding bread behind her back

Gretel

Laravel breadcrumbs right out of a fairy tale.

Gretel is a Laravel package for adding route-based breadcrumbs to your application.

Installation

composer require glhd/gretel

Usage

Defining Breadcrumbs

Gretel adds a new Route macro that you can use when defining your routes:

Single Breadcrumb

In the simplest case, chain the breadcrumb() function onto your existing route to define a breadcrumb:

Route::get('/', HomeController::class)
  ->name('home')
  ->breadcrumb('Home');

Homepage Example

If you need to dynamically control the title, pass in a closure instead:

Route::get('/dashboard', DashboardController::class)
  ->name('dashboard')
  ->breadcrumb(fn() => Auth::user()->name.'’s dashboard');

Dashboard Example

Nested Breadcrumb

Breadcrumbs aren't very useful unless you string them together. Gretel handles nested breadcrumbs by pointing to a previously-defined parent breadcrumb:

Route::get('/users', [UserController::class, 'index'])
  ->name('users.index')
  ->breadcrumb('Users');
  
Route::get('/users/{user}', [UserController::class, 'show'])
  ->name('users.show')
  ->breadcrumb(fn(User $user) => $user->name, 'users.index');

Route::get('/users/{user}/edit', [UserController::class, 'edit'])
  ->name('users.edit')
  ->breadcrumb('Edit', 'users.show');

Nested Route Example

Here, you can see that our users.show route references users.index as its parent. This way, when you render breadcrumbs for users.show it will also show the breadcrumb for users.index.

Gretel assumes that the parameters in nested routes can be safely used for their parent routes. In this example, users.edit will render the users.show breadcrumb using the User value that was resolved for the edit action. In the vast majority of cases, this is exactly what you want. If not, you can override this behavior (see below).

Parent Shorthand

Often, a child route will reference a parent with the same name prefix. In our above example, users.show references users.index and users.edit references users.show. In this case, you can use the parent shorthand:

Route::get('/admin/users/{user}/notes/create', [NotesController::class, 'create'])
  ->name('admin.users.notes.create')
  ->breadcrumb('Add Note', '.index'); // shorthand for "admin.users.notes.index"

This is particularly useful for large apps that have many deeply nested routes.

Shallow Nested Routes

If your nested routes do not contain the route parameters necessary for the parent route, you will need to provide the values to Gretel. You can do this using a third callback:

Route::get('/companies/{company}', [CompanyController::class, 'show'])
  ->name('companies.show')
  ->breadcrumb(fn(Company $company) => $company->name);

Route::get('/users/{user}', [UserController::class, 'show'])
  ->name('users.show')
  ->breadcrumb(fn(User $user) => $user->name, 'companies.show', fn(User $user) => $user->company);

Shallow Nested Example

Resource Routes

You can also define breadcrumbs for resource controllers. The index(), create(), show(), and edit() methods behave exactly like the regular breadcrumb helper except that they automatically set up the parent for you if you don’t provide one.

Route::resource('users', UserController::class)
  ->breadcrumbs(function(ResourceBreadcrumbs $breadcrumbs) {
    $breadcrumbs
      ->index('Users')
      ->create('New User')
      ->show(fn(User $user) => $user->name)
      ->edit('Edit');
  });

If you prefer, you can also use an array syntax for simple resource routes:

Route::resource('users', UserController::class)
  ->breadcrumbs([
    'index' => 'Users',
    'create' => 'New User',
    'show' => fn(User $user) => $user->name,
    'edit' => 'Edit',
  ]);

Vendor Routes

Sometimes you want to register breadcrumbs for routes that are defined in 3rd-party packages. In this case, you can use the Gretel facade directly. The API is exactly the same as the Route::breadcrumb() method, except that you must pass the route name as the first parameter:

Gretel::breadcrumb(
  'teams.show', // Route name
  fn(Team $team) => $team->name, // Title callback
  'profile.show', // Parent
);

Displaying Breadcrumbs

You can display the breadcrumbs for the current route with the <x-breadcrumbs /> Blade component. The Blade component accepts a few optional attributes:

Attribute
framework Render to match a UI framework ("tailwind" by default)
view Render a custom view (supersedes the framework attribute)
jsonld Render as a JSON-LD <script> tag

Supported Frameworks

Gretel supports most common CSS frameworks. We've taken the CSS framework's documented markup and added additional aria- tags where appropriate for better accessibility. Currently supported frameworks:

Tailwind use "tailwind" (default)

Tailwind theme

Materialize use "materialize"

Materialize theme

Bootstrap 5 use "bootstrap5"

Bootstrap 5 theme

Bulma use "bulma"

Bulma theme

Semantic UI use "semantic-ui"

Semantic UI theme

Primer use "primer"

Primer theme

Foundation 6 use "foundation6"

Foundation 6 theme

UIKit use "uikit"

UIKit theme

Older Frameworks

Older versions of some frameworks are also available:

You'll typically want to include the <x-breadcrumbs /> tag somewhere in your application layout (maybe twice if you're using JSON-LD):

layouts/app.blade.php:

<!DOCTYPE html>
<html>
<head>
    <title>{{ $title }}</title>
    <x-breadcrumbs jsonld />
</head>
<body>
<div class="container mx-auto">
    <x-breadcrumbs framework="tailwind" />
    ...
</div>
</body>
</html>

Custom Breadcrumb View

You can render a custom view either by publishing the gretel.php config file via php artisan vendor:publish or by passing a view attribute to the Blade component:

<x-breadcrumbs view="app.breadcrumbs" />

Using Breadcrumbs in Some Other Way

If you need to use the breadcrumbs in some other way—maybe for rending via a client-side framework that Gretel doesn’t already integrate with—you can always just get the current breadcrumbs as a Collection or array off the route:

Route::breadcrumbs()->toCollection();  // Collection of `Breadcrumb` objects
Route::breadcrumbs()->toArray();       // Array of `Breadcrumb` objects
Route::breadcrumbs()->jsonSerialize(); // Array of breadcrumb arrays (title, url, is_current_page)
Route::breadcrumbs()->toJson();        // JSON string of breadcrumbs matching jsonSerialize

For example, our Inertia.js integration could easily be implemented as:

Inertia::share('breadcrumbs', function(Request $request) {
    return $request->route()->breadcrumbs()->jsonSerialize();
});
Accessibility

If you choose to render your own view, please be sure to follow the current WAI-ARIA accessibility best practices. Gretel provides some helpers to make this easier:

@unless ($breadcrumbs->isEmpty())
  <!-- Wrap your breadcrumbs in a <nav> element with an aria-label attribute -->
  <nav aria-label="Breadcrumb">
    <!-- Use an <ol> (ordered list) for the breadcrumb items -->
    <ol>
      @foreach ($breadcrumbs as $breadcrumb)
        <!-- You can use $activeClass() or $inactiveClass() to conditionally apply classes -->
        <li class="{{ $activeClass('active-breadcrumb') }}">
          <!-- Use $ariaCurrent() to apply aria-current="page" to the active breadcrumb -->
            <a href="{{ $breadcrumb->url }}" {{ $ariaCurrent() }}>
              {{ $breadcrumb->title }}
            </a>
        </li>
      @endforeach
    </ol>
  </nav>
@endunless

Caching Breadcrumbs

Because Gretel breadcrumbs are registered alongside your routes, you need to cache your breadcrumbs if you cache your routes. You can do so with the two commands:

# Cache breadcrumbs
php artisan breadcrumbs:cache

# Clear cached breadcrumbs
php artisan breadcrumbs:clear

Please note that you must cache your breadcrumbs before you cache your routes.

Handling Errors

Sometimes you'll mis-configure a breadcrumb or forget to define one. You can register handlers on the Gretel facade to handle these cases:

// Log or report a missing breadcrumb (will always receive a MissingBreadcrumbException instance)
Gretel::handleMissingBreadcrumbs(function(MissingBreadcrumbException $exception) {
  Log::warning($exception->getMessage());
});

// Throw an exception locally if there's a missing breadcrumb
Gretel::throwOnMissingBreadcrumbs(! App::environment('production'));

// Log or report a mis-configured breadcrumb (i.e. a parent route that doesn't exist).
// This handler will pick up any other exception that is triggered while trying to configure
// or render your breadcrumbs, so the type is unknown.
Gretel::handleMisconfiguredBreadcrumbs(function(Throwable $exception) {
  Log::warning($exception->getMessage());
});

// Throw an exception locally if there's a mis-configured breadcrumb
Gretel::throwOnMisconfiguredBreadcrumbs(! App::environment('production'));

Integration With Third Party Packages

Gretel automatically shares your breadcrumbs with Inertia.js if you have that package installed. You don't need to do anything to enable this integration. (If you do not want this behavior for some reason, you can disable it by publishing the Gretel config.)

Your breadcrumbs will be available in your client code as breadcrumbs and look something like:

const breadcrumbs = [
  {
    title: 'Home',
    url: 'https://www.yourapp.com',
    is_current_page: false,
  }, {
    title: 'Users',
    url: 'https://www.yourapp.com/users',
    is_current_page: false,
  }, {
    title: 'Add a User',
    url: 'https://www.yourapp.com/users/create',
    is_current_page: true,
  }
];

You can then render the breadcrumbs in the client however you see fit. Be sure to review the custom breadcrumbs section for information about how to ensure that your client-side breadcrumbs are fully accessible.