From 32620ed8ffe6e6c8552b59f4dbde0ebd17725151 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 12 Jun 2026 08:56:11 +0100 Subject: [PATCH 1/7] avoid calling `value` unless we know it exists --- src/Cascade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cascade.php b/src/Cascade.php index 0e718598..776468ec 100644 --- a/src/Cascade.php +++ b/src/Cascade.php @@ -654,7 +654,7 @@ protected function jsonLd() return [ '@type' => 'ListItem', 'position' => $index + 1, - 'name' => $crumb->value('title'), + 'name' => method_exists($crumb, 'value') ? $crumb->value('title') : $crumb->get('title'), 'item' => $crumb->absoluteUrl(), ]; })->all(), From a65c41f5486602480d92ada052f3c192ef4f291a Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 12 Jun 2026 09:48:11 +0100 Subject: [PATCH 2/7] call the property instead --- src/Cascade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cascade.php b/src/Cascade.php index 776468ec..0908dba6 100644 --- a/src/Cascade.php +++ b/src/Cascade.php @@ -654,7 +654,7 @@ protected function jsonLd() return [ '@type' => 'ListItem', 'position' => $index + 1, - 'name' => method_exists($crumb, 'value') ? $crumb->value('title') : $crumb->get('title'), + 'name' => $crumb->title, 'item' => $crumb->absoluteUrl(), ]; })->all(), From 124575fea82245a8b8245c3c525b112ecf7b5845 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 12 Jun 2026 09:56:20 +0100 Subject: [PATCH 3/7] add tests --- tests/CascadeTest.php | 93 +++++++++++++++++++++++++++++++++ tests/Localized/CascadeTest.php | 40 ++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/tests/CascadeTest.php b/tests/CascadeTest.php index ae440172..bf72cd66 100644 --- a/tests/CascadeTest.php +++ b/tests/CascadeTest.php @@ -522,4 +522,97 @@ public function it_generates_json_ld_breadcrumbs() '{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://cool-runnings.com"},{"@type":"ListItem","position":2,"name":"\'Dance Like No One is Watching\' Is Bad Advice","item":"http://cool-runnings.com/dance"}]}', ], $data['json_ld']->all()); } + + #[Test] + public function it_generates_json_ld_breadcrumbs_for_entry() + { + Collection::findByHandle('articles')->routes('articles/{slug}')->save(); + + $siteDefaults = SiteDefaults::in('default')->set([ + 'json_ld_breadcrumbs' => true, + ]); + + $this->get('/articles/dance'); + + $data = (new Cascade) + ->with($siteDefaults->all()) + ->get(); + + $breadcrumbs = collect($data['json_ld'])->first(fn ($snippet) => str_contains($snippet, 'BreadcrumbList')); + + $this->assertNotNull($breadcrumbs); + + $decoded = json_decode($breadcrumbs, true); + + $this->assertEquals('BreadcrumbList', $decoded['@type']); + $this->assertCount(3, $decoded['itemListElement']); + + $this->assertEquals([ + '@type' => 'ListItem', + 'position' => 1, + 'name' => 'Home', + 'item' => 'http://cool-runnings.com', + ], $decoded['itemListElement'][0]); + + $this->assertEquals([ + '@type' => 'ListItem', + 'position' => 2, + 'name' => 'Articles', + 'item' => 'http://cool-runnings.com/articles', + ], $decoded['itemListElement'][1]); + + $this->assertEquals([ + '@type' => 'ListItem', + 'position' => 3, + 'name' => "'Dance Like No One is Watching' Is Bad Advice", + 'item' => 'http://cool-runnings.com/articles/dance', + ], $decoded['itemListElement'][2]); + } + + #[Test] + public function it_generates_json_ld_breadcrumbs_for_taxonomy_term() + { + $siteDefaults = SiteDefaults::in('default')->set([ + 'json_ld_breadcrumbs' => true, + ]); + + $this->files->makeDirectory(resource_path('views/topics'), force: true); + $this->files->put(resource_path('views/topics/index.antlers.html'), ''); + + $this->get('/topics/sneakers'); + + $data = (new Cascade) + ->with($siteDefaults->all()) + ->get(); + + $breadcrumbs = collect($data['json_ld'])->first(fn ($snippet) => str_contains($snippet, 'BreadcrumbList')); + + $this->assertNotNull($breadcrumbs); + + $decoded = json_decode($breadcrumbs, true); + + $this->assertEquals('BreadcrumbList', $decoded['@type']); + $this->assertCount(3, $decoded['itemListElement']); + + $this->assertEquals([ + '@type' => 'ListItem', + 'position' => 1, + 'name' => 'Home', + 'item' => 'http://cool-runnings.com', + ], $decoded['itemListElement'][0]); + + $this->assertEquals([ + '@type' => 'ListItem', + 'position' => 2, + 'name' => 'Topics', + 'item' => 'http://cool-runnings.com/topics', + ], $decoded['itemListElement'][1]); + + $this->assertEquals([ + '@type' => 'ListItem', + 'position' => 3, + 'name' => 'Sneakers', + 'item' => 'http://cool-runnings.com/topics/sneakers', + ], $decoded['itemListElement'][2]); + } } diff --git a/tests/Localized/CascadeTest.php b/tests/Localized/CascadeTest.php index 797b18e5..b61bad36 100644 --- a/tests/Localized/CascadeTest.php +++ b/tests/Localized/CascadeTest.php @@ -67,4 +67,44 @@ public function it_generates_seo_cascade_for_canonical_url_and_handles_duplicate 'it' => 'http://corse-fantastiche.it', ], collect($data['alternate_locales'])->pluck('url', 'hreflang')->all()); } + + #[Test] + public function it_generates_json_ld_breadcrumbs_for_entry_using_title_from_origin() + { + // The French /about entry has an origin but no title set, + // so it should use the origin entry's title in breadcrumbs + $siteDefaults = SiteDefaults::in('french')->set([ + 'json_ld_breadcrumbs' => true, + ]); + + $this->get('http://cool-runnings.com/fr/about'); + + $data = (new Cascade) + ->with($siteDefaults->all()) + ->get(); + + $breadcrumbs = collect($data['json_ld'])->first(fn ($snippet) => str_contains($snippet, 'BreadcrumbList')); + + $this->assertNotNull($breadcrumbs); + + $decoded = json_decode($breadcrumbs, true); + + $this->assertEquals('BreadcrumbList', $decoded['@type']); + $this->assertCount(2, $decoded['itemListElement']); + + $this->assertEquals([ + '@type' => 'ListItem', + 'position' => 1, + 'name' => 'Home', + 'item' => 'http://cool-runnings.com/fr', + ], $decoded['itemListElement'][0]); + + // The localized entry has no title, so it should use the origin's title + $this->assertEquals([ + '@type' => 'ListItem', + 'position' => 2, + 'name' => 'About', + 'item' => 'http://cool-runnings.com/fr/about', + ], $decoded['itemListElement'][1]); + } } From 8574793b752fabefc1222c7844a379fc06f0d18c Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 12 Jun 2026 10:04:35 +0100 Subject: [PATCH 4/7] only assert the last breadcrumb --- tests/Localized/CascadeTest.php | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/tests/Localized/CascadeTest.php b/tests/Localized/CascadeTest.php index b61bad36..6730562a 100644 --- a/tests/Localized/CascadeTest.php +++ b/tests/Localized/CascadeTest.php @@ -90,21 +90,9 @@ public function it_generates_json_ld_breadcrumbs_for_entry_using_title_from_orig $decoded = json_decode($breadcrumbs, true); $this->assertEquals('BreadcrumbList', $decoded['@type']); - $this->assertCount(2, $decoded['itemListElement']); - $this->assertEquals([ - '@type' => 'ListItem', - 'position' => 1, - 'name' => 'Home', - 'item' => 'http://cool-runnings.com/fr', - ], $decoded['itemListElement'][0]); - - // The localized entry has no title, so it should use the origin's title - $this->assertEquals([ - '@type' => 'ListItem', - 'position' => 2, - 'name' => 'About', - 'item' => 'http://cool-runnings.com/fr/about', - ], $decoded['itemListElement'][1]); + $lastItem = end($decoded['itemListElement']); + $this->assertEquals('About', $lastItem['name']); + $this->assertEquals('http://cool-runnings.com/fr/about', $lastItem['item']); } } From 9bacbfa573d3d2480afe382876072495863883c5 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 12 Jun 2026 10:04:53 +0100 Subject: [PATCH 5/7] wip --- tests/CascadeTest.php | 30 ++++++++++++------------------ tests/Localized/CascadeTest.php | 9 +++------ 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/tests/CascadeTest.php b/tests/CascadeTest.php index bf72cd66..979bfe34 100644 --- a/tests/CascadeTest.php +++ b/tests/CascadeTest.php @@ -539,34 +539,31 @@ public function it_generates_json_ld_breadcrumbs_for_entry() ->get(); $breadcrumbs = collect($data['json_ld'])->first(fn ($snippet) => str_contains($snippet, 'BreadcrumbList')); + $breadcrumbs = json_decode($breadcrumbs, true); - $this->assertNotNull($breadcrumbs); - - $decoded = json_decode($breadcrumbs, true); - - $this->assertEquals('BreadcrumbList', $decoded['@type']); - $this->assertCount(3, $decoded['itemListElement']); + $this->assertEquals('BreadcrumbList', $breadcrumbs['@type']); + $this->assertCount(3, $breadcrumbs['itemListElement']); $this->assertEquals([ '@type' => 'ListItem', 'position' => 1, 'name' => 'Home', 'item' => 'http://cool-runnings.com', - ], $decoded['itemListElement'][0]); + ], $breadcrumbs['itemListElement'][0]); $this->assertEquals([ '@type' => 'ListItem', 'position' => 2, 'name' => 'Articles', 'item' => 'http://cool-runnings.com/articles', - ], $decoded['itemListElement'][1]); + ], $breadcrumbs['itemListElement'][1]); $this->assertEquals([ '@type' => 'ListItem', 'position' => 3, 'name' => "'Dance Like No One is Watching' Is Bad Advice", 'item' => 'http://cool-runnings.com/articles/dance', - ], $decoded['itemListElement'][2]); + ], $breadcrumbs['itemListElement'][2]); } #[Test] @@ -586,33 +583,30 @@ public function it_generates_json_ld_breadcrumbs_for_taxonomy_term() ->get(); $breadcrumbs = collect($data['json_ld'])->first(fn ($snippet) => str_contains($snippet, 'BreadcrumbList')); + $breadcrumbs = json_decode($breadcrumbs, true); - $this->assertNotNull($breadcrumbs); - - $decoded = json_decode($breadcrumbs, true); - - $this->assertEquals('BreadcrumbList', $decoded['@type']); - $this->assertCount(3, $decoded['itemListElement']); + $this->assertEquals('BreadcrumbList', $breadcrumbs['@type']); + $this->assertCount(3, $breadcrumbs['itemListElement']); $this->assertEquals([ '@type' => 'ListItem', 'position' => 1, 'name' => 'Home', 'item' => 'http://cool-runnings.com', - ], $decoded['itemListElement'][0]); + ], $breadcrumbs['itemListElement'][0]); $this->assertEquals([ '@type' => 'ListItem', 'position' => 2, 'name' => 'Topics', 'item' => 'http://cool-runnings.com/topics', - ], $decoded['itemListElement'][1]); + ], $breadcrumbs['itemListElement'][1]); $this->assertEquals([ '@type' => 'ListItem', 'position' => 3, 'name' => 'Sneakers', 'item' => 'http://cool-runnings.com/topics/sneakers', - ], $decoded['itemListElement'][2]); + ], $breadcrumbs['itemListElement'][2]); } } diff --git a/tests/Localized/CascadeTest.php b/tests/Localized/CascadeTest.php index 6730562a..f0a9d151 100644 --- a/tests/Localized/CascadeTest.php +++ b/tests/Localized/CascadeTest.php @@ -84,14 +84,11 @@ public function it_generates_json_ld_breadcrumbs_for_entry_using_title_from_orig ->get(); $breadcrumbs = collect($data['json_ld'])->first(fn ($snippet) => str_contains($snippet, 'BreadcrumbList')); + $breadcrumbs = json_decode($breadcrumbs, true); - $this->assertNotNull($breadcrumbs); + $this->assertEquals('BreadcrumbList', $breadcrumbs['@type']); - $decoded = json_decode($breadcrumbs, true); - - $this->assertEquals('BreadcrumbList', $decoded['@type']); - - $lastItem = end($decoded['itemListElement']); + $lastItem = end($breadcrumbs['itemListElement']); $this->assertEquals('About', $lastItem['name']); $this->assertEquals('http://cool-runnings.com/fr/about', $lastItem['item']); } From fe98b924f8a9b994b82f567a6c0902c252be5b10 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 12 Jun 2026 10:05:16 +0100 Subject: [PATCH 6/7] wip --- tests/Localized/CascadeTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Localized/CascadeTest.php b/tests/Localized/CascadeTest.php index f0a9d151..476f2c05 100644 --- a/tests/Localized/CascadeTest.php +++ b/tests/Localized/CascadeTest.php @@ -71,8 +71,6 @@ public function it_generates_seo_cascade_for_canonical_url_and_handles_duplicate #[Test] public function it_generates_json_ld_breadcrumbs_for_entry_using_title_from_origin() { - // The French /about entry has an origin but no title set, - // so it should use the origin entry's title in breadcrumbs $siteDefaults = SiteDefaults::in('french')->set([ 'json_ld_breadcrumbs' => true, ]); From 839a77343af51f07421abd6f5029a8441c5e3ba6 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 12 Jun 2026 10:46:44 +0100 Subject: [PATCH 7/7] bump statamic/cms to ^6.19.0 it includes https://github.com/statamic/cms/pull/13789 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5259d06a..2d43eabc 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ } }, "require": { - "statamic/cms": "^6.10", + "statamic/cms": "^6.19", "pixelfear/composer-dist-plugin": "^0.1.6", "spatie/simple-excel": "^3.9" },