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

Schema Breadcrumb Bundle Laravel Package

exs/schema-breadcrumb-bundle

Symfony bundle that generates schema.org BreadcrumbList JSON-LD script tags from Twig. Pass an ordered array of breadcrumb items with name, URL, and optional image to the getJsonBreadcrumb() function to output valid structured data for search engines.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: it is not a Laravel package. exs/schema-breadcrumb-bundle is a Symfony bundle registered through AppKernel::registerBundles(), exposing a Twig global getJsonBreadcrumb(). There is no service provider, no facade, no config file to publish, and no auto-discovery. Running composer require exs/schema-breadcrumb-bundle in a Laravel app will either fail on Symfony 2/3 + Twig 1.x constraints or pin your platform to an unsupported PHP version. Treat the repo as a specification, not a dependency.

What you actually adopt is the contract it defines — an ordered array of {name, url, image} mapped to schema.org BreadcrumbList:

{
  "@context": "http://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "item": { "@id": "...", "name": "...", "image": "..." } }
  ]
}

Where to look first. Open the README's generated-output block and copy it as your golden fixture. That JSON is the entire behavioral surface of the package — the rest is Symfony wiring you will discard.

First use case. The one that pays for itself: a product/category page whose breadcrumb already exists in your router or menu. Day one in Laravel looks like this:

// app/Support/StructuredData/BreadcrumbList.php
namespace App\Support\StructuredData;

class BreadcrumbList
{
    public static function toJson(array $items): string
    {
        $list = [];
        foreach (array_values($items) as $i => $item) {
            $entry = array_filter([
                '@id'   => $item['url']   ?? null,
                'name'  => $item['name']  ?? null,
                'image' => $item['image'] ?? null,
            ], fn ($v) => $v !== null);

            $list[] = [
                '@type'    => 'ListItem',
                'position' => $i + 1,
                'item'     => $entry,
            ];
        }

        return json_encode([
            '@context'        => 'https://schema.org',
            '@type'           => 'BreadcrumbList',
            'itemListElement' => $list,
        ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
    }
}

Then a Blade component, so templates stay as clean as the Twig original:

{{-- resources/views/components/breadcrumb-schema.blade.php --}}
@props(['items'])
<script type="application/ld+json">{!! \App\Support\StructuredData\BreadcrumbList::toJson($items) !!}</script>
{{-- in your layout <head> --}}
<x-breadcrumb-schema :items="[
    ['name' => 'Category One', 'url' => url('/category-one'), 'image' => asset('images/category-one.png')],
    ['name' => 'Subcategory Two', 'url' => url('/category-one/subcategory-two')],
    ['name' => 'Element Three', 'url' => url('/category-one/subcategory-two/element-three')],
]" />

Total migration cost: well under a day, and you now own the code.

Implementation Patterns

Derive items from the router, not from hand-written arrays. The README pattern (paste an array into the template) drifts the moment a route slug changes. Build the list from the same source of truth your <nav> uses:

// App\Support\StructuredData\Breadcrumbs::fromCollection(...)
$items = $breadcrumbs->map(fn ($b) => [
    'name' => $b->label,
    'url'  => route($b->route, $b->params),
])->all();

If you use diglactic/laravel-breadcrumbs or spatie/laravel-navigation, wrap its output once and reuse it for both the visible trail and the JSON-LD. One generator, two renderers.

Prefer an explicit DTO over array-order hierarchy. The README's "order of the elements in the array defines the breadcrumb" is the package's biggest design smell — reordering a loop silently corrupts your structured data. Define a small value object with name, url, image, and either compute position at render time (as above) or make it explicit.

Emit one JSON-LD block per page, not per component. Search engines parse fine with multiple <script type="application/ld+json"> tags, but you'll want a single @graph as soon as breadcrumbs are joined by Organization, Product, BreadcrumbList, or FAQPage. Structure now for that:

// App\Support\StructuredData\Graph
$graph = Graph::make()
    ->add(BreadcrumbList::schema($items))
    ->add(Product::schema($product));

echo $graph->toScript();

Retrofitting a @graph later means touching every template. Do it while there's one node.

Blade component in the layout, not in every view. Put <x-breadcrumb-schema> in your app.blade.php head and pass items via a view composer or @section. That mirrors the package's intended "one call in <head>" usage and keeps per-page templates from forgetting it.

If you truly are on legacy Symfony 2/3, the package does genuinely work as documented: register the bundle, call {{ getJsonBreadcrumb(items) }} in <head>, done. That's the only stack where "install it" is a defensible recommendation — and the Travis-CI-deprecated, README-only maturity signals still mean you should pin the commit hash.

Feature test the output, not the helper. Snapshot the rendered tag so a future refactor can't silently break indexing:

it('emits breadcrumb JSON-LD', function () {
    $response = $this->get('/category-one/subcategory-two/element-three');

    $json = Str::between($response->getContent(), '<script type="application/ld+json">', '</script>');
    $data = json_decode($json, true, flags: JSON_THROW_ON_ERROR);

    expect($data['@type'])->toBe('BreadcrumbList')
        ->and($data['itemListElement'])->toHaveCount(3)
        ->and($data['itemListElement'][0]['position'])->toBe(1);
});

Gotchas and Tips

XSS in a script context is the real one. The original package interpolates values into <script> — json_encode alone is not safe inside <script> (a </script> sequence in a product name terminates the block early). Use json_encode(..., JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) and render with {!! !!} only after that, or JSON_THROW_ON_ERROR plus a ScriptTag wrapper. Never {!! $userSupplied !!} unencoded.

json_encode returns false on invalid UTF-8. With JSON_THROW_ON_ERROR you get a JsonException you can catch and log; without it you emit a blank, broken tag that fails Rich Results silently. Always pass the flag.

Use https://schema.org, not http://schema.org. The README (and the package output) uses http://. Google accepts both today but the canonical context is HTTPS; matching the README byte-for-byte is a trap.

position must be 1-indexed and contiguous. Skip a breadcrumb conditionally (e.g., unpublished parent) and a naive $loop->iteration over a filtered array is still correct — but a hardcoded index or a position pulled from a menu item's own ordering will not be. Number at render time from array_values().

URLs must be absolute. url('/foo') and route('foo') give you that; asset() does for images. Relative @id values are the single most common Rich Results validation failure for this schema type.

Omit keys rather than emitting nulls. "image": null is a validation warning. array_filter($item, fn ($v) => $v !== null) before encoding, as in the snippet above.

Don't point @id at a fragment. For breadcrumbs the item.@id should be the page URL. If your breadcrumb entries are anchors on the same page (#section-two), collapse them to the parent page URL or drop them — Google won't render anchors as trail steps.

The package has no tests, no version constraints, and no CVE process. If you do adopt it on legacy Symfony, vendor/pin it (a specific commit in composer.json, not ^1.0) and add your own golden-file test around the rendered output. There is no upstream to notify you when it breaks.

Reach for spatie/schema-org if you need more than breadcrumbs. It's Laravel-native, actively maintained, supports graphs and every schema type, and is the natural upgrade path. The trade-off is more API surface; the build-it-yourself route above stays the better choice if breadcrumbs really are the only thing you'll ever emit.

Validate in CI once, manually thereafter. Add Google's Rich Results Test to your release checklist, and consider a spatie/schema-org-style snapshot test so schema regressions surface as failing tests rather than as a slow decline in SERP appearance.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console