Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Breadcrumbs Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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

  1. The Defining Breadcrumbs section of the README — the parent()/push() mental model is the whole package.
  2. 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.
  3. The Route-Bound Breadcrumbs section — this is the feature that removes per-page boilerplate and is usually the reason teams keep the package.

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.

Implementation Patterns

Route-bound breadcrumbs (the default pattern for a mature app)

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.

Build the trail from parent(), not from copy-paste

Always 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.

Arrow functions for the flat, one-liner trails

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))
);

Nested/recursive hierarchies

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.

Non-linked and section-only crumbs

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.

Pairing with JSON-LD for SEO

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.

Custom template

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) }}

Bypassing the view entirely

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.

Organizing as the file grows

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.

Gotchas and Tips

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.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
encore2/laravel-admin2
cboxdk/laravel-console-kit
shapecode/doctrine-dbal-datetimeutc
gilads-otiannoh254/inertia-protocol
dakiquang/laravel-oauth-refresh-lock
woda/laravel-worktrees
dalefranks/ramasite
faghy/laracashbook
roundly-consulting/google-places-for-laravel
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
adachsoft/cli-process-tester
adachsoft/changelog-linter
adachsoft/collection
adachsoft/php-code-style
adachsoft/api-version-bundle
cgv/localized-resource