diglactic/laravel-breadcrumbs
A simple, Laravel-style way to create breadcrumbs. Define breadcrumbs alongside routes, output them with custom templates, generate structured data, and bind breadcrumbs to route models. Official fork of Dave James Miller's classic package.
Install and wire up
composer require diglactic/laravel-breadcrumbs
The service provider is auto-discovered, so there is no config/app.php edit. The package will pick up routes/breadcrumbs.php at boot time if the file exists — that is the only file you must create.
<?php // routes/breadcrumbs.php
use Diglactic\Breadcrumbs\Breadcrumbs;
use Diglactic\Breadcrumbs\Generator as BreadcrumbTrail;
Breadcrumbs::for('home', function (BreadcrumbTrail $trail) {
$trail->push('Home', route('home'));
});
Breadcrumbs::for('posts.index', function (BreadcrumbTrail $trail) {
$trail->parent('home');
$trail->push('Blog', route('posts.index'));
});
Breadcrumbs::for('posts.show', function (BreadcrumbTrail $trail, $post) {
$trail->parent('posts.index');
$trail->push($post->title, route('posts.show', $post));
});
First render — drop it into your layout, above @yield('content'):
{{ Breadcrumbs::render('posts.show', $post) }}
render() returns an HtmlString, so plain {{ }} is correct and safe (no {!! !!} needed).
Where to look first
parent()/push() mental model is the whole package.config/breadcrumbs.php — publish it early with php artisan vendor:publish --tag=breadcrumbs-config to swap the default Bootstrap 5 template for breadcrumbs::tailwind, breadcrumbs::json-ld, or your own view.First real use case: a resource CRUD (posts.index → posts.show → posts.edit) rendered once in the layout. You delete Breadcrumbs::render(...) from every Blade view and call the argument-less form instead.
Name breadcrumb definitions identically to your route names, then call render() with no arguments anywhere:
// routes/web.php
Route::get('/', [HomeController::class, 'index'])->name('home');
Route::get('/posts', [PostController::class, 'index'])->name('posts.index');
Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show');
{{-- resources/views/layouts/app.blade.php --}}
@if (Breadcrumbs::exists())
{{ Breadcrumbs::render() }}
@endif
render() resolves Route::currentRouteName() and forwards the route parameters positionally into your closure. The @if (Breadcrumbs::exists()) guard is what makes a layout-level render survivable — otherwise error pages, auth screens, and half-built routes blow up.
parent(), not from copy-pasteAlways chain via parent() rather than repeating push() calls. It keeps the hierarchy in one place, and a change to the section title propagates everywhere:
Breadcrumbs::for('posts.edit', function (BreadcrumbTrail $trail, $post) {
$trail->parent('posts.show', $post); // forward the model
$trail->push('Edit', route('posts.edit', $post));
});
Parents accept arguments too — pass the model down so posts.show renders the real title, not a placeholder.
For the ~80% of definitions that are two calls, use the fluent form. It reads like a route file and diffs cleanly:
Breadcrumbs::for('posts.show',
fn (BreadcrumbTrail $trail, Post $post) => $trail
->parent('posts.index')
->push($post->title, route('posts.show', $post))
);
For self-referencing trees (categories, pages, org units), recurse through parent():
Breadcrumbs::for('categories.show', function (BreadcrumbTrail $trail, Category $category) {
$category->parent
? $trail->parent('categories.show', $category->parent)
: $trail->parent('home');
$trail->push($category->title, route('categories.show', $category));
});
Prefer this over a foreach ($category->ancestors ...) loop when the tree is deep and you also want the ancestor pages to be independently routable — you get one definition covering every level.
push() accepts null as the URL. Use it for grouping levels that have no landing page (a "Settings" crumb above "Profile", for example). The built-in templates handle null fine.
Render twice: structured data in <head>, visible trail in the body.
<head>
{{ Breadcrumbs::view('breadcrumbs::json-ld', 'posts.show', $post) }}
</head>
<body>
{{ Breadcrumbs::render('posts.show', $post) }}
</body>
Extra keys passed as the third argument to push() land on the breadcrumb object and are available to the template — ['image' => asset($post->cover)] is what the JSON-LD view reads.
Publish the built-ins to get a starting point, then point config at your own view:
php artisan vendor:publish --tag=breadcrumbs-views
// config/breadcrumbs.php
'view' => 'partials.breadcrumbs',
The view receives a $breadcrumbs Collection; each item exposes title, url, plus your custom data keys. Switch views at runtime without touching config:
{{ Breadcrumbs::view('partials.breadcrumbs-compact', 'posts.show', $post) }}
When you need the data, not the markup — a JSON API field, a sitemap, an Inertia/Vue prop, a Livewire component:
$trail = Breadcrumbs::generate('posts.show', $post);
// or, route-bound:
$trail = Breadcrumbs::generate();
$crumbs = $trail->map(fn ($c) => ['title' => $c->title, 'url' => $c->url]);
generate() is the seam for testing too: assert against titles/URLs without rendering Blade.
Split once routes/breadcrumbs.php passes ~200 lines. Keep it as the entry point and require domain files from it:
require __DIR__ . '/breadcrumbs/admin.php';
require __DIR__ . '/breadcrumbs/shop.php';
Every included file still runs inside the same global registry, so cross-file parent() calls work unchanged.
The last crumb is intentionally not a link. All shipped templates omit the anchor on the final item even if you gave it a URL. Don't "fix" this by editing the definition — copy the template and change the @if (!is_null($breadcrumb->url) && !$loop->last) condition if you genuinely need a self-link.
Route-bound parameter order is positional, not by name. A closure signature function ($trail, $post, $comment) must match the URI segment order in /posts/{post}/comments/{comment}. Reordering segments in web.php silently mismatches arguments. Type-hint the params (Post $post) to get a loud TypeError instead of a wrong title.
Route-bound breadcrumbs need real model binding. The closure receives whatever Route::current()->parameters() holds. With implicit binding you get an Eloquent model and Post $post works. Without it you get a string and the type hint throws. Bind explicitly (Route::model(...)) or drop the type hint.
Missing/duplicate names throw, not return null. Calling render('typo.name') raises an exception during the response — you'll see it as a 500 on that page only. Same for defining Breadcrumbs::for('home', ...) twice. Grep for duplicate names when a page suddenly breaks after a merge; the exceptions here are deliberate rather than a soft fallback.
Breadcrumbs::exists() takes an optional name. Use Breadcrumbs::exists() (no args) in a shared layout, and Breadcrumbs::exists('posts.show') in a targeted @if inside a specific view. Guarding the layout call is the single highest-value habit with this package.
Every render re-evaluates every closure. Two renders (head + body JSON-LD) means the whole chain — including $post->parent->parent recursion — runs twice. On deep category trees that's a stack of queries per request. Eager-load or memoize ancestors on the model, or compute generate() once in a view composer and hand the collection to both templates via view().
routes/breadcrumbs.php runs on every request. It's a definition file, not a cached artifact. Never put queries, env() calls, or heavy computation directly in it — only inside the closures, which are lazy.
JSON-LD needs absolute URLs. Use route()/url() everywhere; relative strings produce structurally invalid markup that Google silently ignores. Also note the README's warning: if you run renatomarinho/laravel-page-speed, the TrimUrls middleware strips the https:// and breaks the JSON-LD template — disable that middleware, don't try to out-encode it.
Escaping is already handled. render() and view() return HtmlString. If you wrap them in {!! !!} you gain nothing; if you accidentally e() the result you'll see raw <ol> on the page.
Breadcrumb names are just array keys — dots and hyphens are fine. posts.show and admin.users.index work as-is and let you mirror route names 1:1, which is the whole point of route-bound rendering. Don't invent a second naming scheme.
Recursion protection is your job. A self-referencing parent_id bug (a category that is its own ancestor) turns into infinite recursion inside the closure and a stack exhaustion rather than a clean exception. Validate the tree on save, or cap depth in the closure.
Test at the generate() layer. Breadcrumbs::generate('posts.show', $post) in a feature test with assertCount/assertSame on titles catches 90% of definition regressions without brittle HTML assertions. Add one Blade assertion for the template itself.
Version pinning matters across Laravel majors. Laravel 10–13 all map to diglactic/laravel-breadcrumbs 10.x, Laravel 9 needs 9.x, 8 and below need 8.x. This is a fork of the original Dave James Miller package, so old tutorials/StackOverflow answers referencing DaveJamesMiller\Breadcrumbs\... namespaces are the same API — swap the namespace and they generally apply. When upgrading Laravel, bump this package in the same PR or the service provider registration will fail.
Extension points, in ascending order of invasiveness: custom data via push()'s third arg → custom Blade template → runtime view switching with view() → generate() and rolling your own output. Reach for the template before you subclass anything; there's no reason to wrap the facade in a repository.
How can I help you explore Laravel packages today?