From 62b7b52a357e2ab555ee46a592f5089cc9c1c113 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Fri, 21 Aug 2026 20:26:06 +0500 Subject: [PATCH 1/3] Fix nav entry links losing their domain across sites --- src/Structures/AugmentedPage.php | 10 +++++++++ src/Structures/Page.php | 25 +++++++++++++++++++-- tests/Data/Structures/PageTest.php | 14 +++++++----- tests/Tags/StructureTagTest.php | 35 ++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 8 deletions(-) diff --git a/src/Structures/AugmentedPage.php b/src/Structures/AugmentedPage.php index 5a91172fbd1..76dfb541e74 100644 --- a/src/Structures/AugmentedPage.php +++ b/src/Structures/AugmentedPage.php @@ -61,6 +61,16 @@ protected function getFromData($key) return $this->page->getSupplement($key) ?? $this->page->value($key); } + protected function url() + { + return $this->page->url(); + } + + protected function urlWithoutRedirect() + { + return $this->page->urlWithoutRedirect(); + } + public function blueprintFields() { if ($this->fieldsCache) { diff --git a/src/Structures/Page.php b/src/Structures/Page.php index 207d3eeeead..1f64ac48fe6 100644 --- a/src/Structures/Page.php +++ b/src/Structures/Page.php @@ -79,12 +79,33 @@ public function setUrl($url) public function url() { - return $this->url ?? optional($this->entry())->url(); + if ($this->url) { + return $this->url; + } + + if (! $entry = $this->entry()) { + return null; + } + + return $this->selectsAcrossSites() ? $entry->absoluteUrl() : $entry->url(); } public function urlWithoutRedirect() { - return $this->url ?? optional($this->entry())->urlWithoutRedirect(); + if ($this->url) { + return $this->url; + } + + if (! $entry = $this->entry()) { + return null; + } + + return $this->selectsAcrossSites() ? $entry->absoluteUrlWithoutRedirect() : $entry->urlWithoutRedirect(); + } + + private function selectsAcrossSites() + { + return $this->structure() instanceof Nav && $this->structure()->canSelectAcrossSites(); } public function isRedirect() diff --git a/tests/Data/Structures/PageTest.php b/tests/Data/Structures/PageTest.php index 83d42d3a630..63cd7d1b2ca 100644 --- a/tests/Data/Structures/PageTest.php +++ b/tests/Data/Structures/PageTest.php @@ -243,9 +243,10 @@ public function it_gets_the_entrys_uri_when_the_structure_does_not_have_a_collec $entry->shouldReceive('uri')->andReturn('/the/actual/entry/uri'); $entry->shouldReceive('value')->with('redirect')->andReturnNull(); - $tree = $this->newTree()->setStructure( - $this->mock(Nav::class) - ); + $nav = $this->mock(Nav::class); + $nav->shouldReceive('canSelectAcrossSites')->andReturnFalse(); + + $tree = $this->newTree()->setStructure($nav); $page = (new Page) ->setTree($tree) @@ -269,9 +270,10 @@ public function it_gets_the_uri_of_a_redirect_entry() $entry->shouldReceive('uri')->andReturn('/the/actual/entry/uri'); $entry->shouldReceive('value')->with('redirect')->andReturn('http://example.com/page'); - $tree = $this->newTree()->setStructure( - $this->mock(Nav::class) - ); + $nav = $this->mock(Nav::class); + $nav->shouldReceive('canSelectAcrossSites')->andReturnFalse(); + + $tree = $this->newTree()->setStructure($nav); $page = (new Page) ->setTree($tree) diff --git a/tests/Tags/StructureTagTest.php b/tests/Tags/StructureTagTest.php index 1c23d02ef9d..84b49d9d67e 100644 --- a/tests/Tags/StructureTagTest.php +++ b/tests/Tags/StructureTagTest.php @@ -9,6 +9,7 @@ use Statamic\Facades\Collection; use Statamic\Facades\Entry; use Statamic\Facades\Nav; +use Statamic\Facades\Site; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -455,6 +456,40 @@ public function it_sets_is_current_and_is_parent_for_a_nav_when_home_is_an_entry $this->assertEquals('[home][1=parent][1-1=parent][1-1-1=current][1-1-1-1][2][3]', $result); } + #[Test] + public function it_uses_the_absolute_url_for_a_nav_entry_link_on_another_site() + { + $this->setSites([ + 'en' => ['url' => 'http://one.example.com/', 'locale' => 'en'], + 'fr' => ['url' => 'http://two.example.com/', 'locale' => 'fr'], + ]); + + Site::setCurrent('en'); + + tap(Collection::make('pages')->routes('{slug}'))->sites(['en', 'fr'])->save(); + + EntryFactory::id('projects')->collection('pages')->locale('en')->slug('projects')->data(['title' => 'Projects'])->create(); + EntryFactory::id('projects-fr')->origin('projects')->collection('pages')->locale('fr')->slug('projects')->data(['title' => 'Projects'])->create(); + + $nav = Nav::make('test')->canSelectAcrossSites(true); + $nav->makeTree('en', [ + ['id' => 'link', 'title' => 'Projects', 'entry' => 'projects-fr'], + ])->save(); + $nav->save(); + + $urlTemplate = '{{ nav:test }}{{ url }}{{ /nav:test }}'; + + $this->assertEquals('http://two.example.com/projects', (string) Antlers::parse($urlTemplate, [], true)); + + $mock = \Mockery::mock(\Statamic\Facades\URL::getFacadeRoot())->makePartial(); + \Statamic\Facades\URL::swap($mock); + $mock->shouldReceive('getCurrent')->andReturn('/projects'); + + $currentTemplate = '{{ nav:test }}{{ if is_current }}current{{ else }}not-current{{ /if }}{{ /nav:test }}'; + + $this->assertEquals('not-current', (string) Antlers::parse($currentTemplate, [], true)); + } + #[Test] public function it_sets_is_parent_based_on_the_url_too() { From db78fa8db7678188cb27e4a13424a9de3eb227ed Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Fri, 21 Aug 2026 20:30:35 +0500 Subject: [PATCH 2/3] Stub Page::url in AugmentedPageTest and split the cross-site nav test --- tests/Data/Structures/AugmentedPageTest.php | 1 + tests/Tags/StructureTagTest.php | 39 +++++++++++++-------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/tests/Data/Structures/AugmentedPageTest.php b/tests/Data/Structures/AugmentedPageTest.php index 39e2e863bf4..30c3bfebe9c 100644 --- a/tests/Data/Structures/AugmentedPageTest.php +++ b/tests/Data/Structures/AugmentedPageTest.php @@ -200,6 +200,7 @@ public function it_gets_values_from_the_entry() $page->shouldReceive('data')->andReturn(collect(['one' => 'dos', 'three' => 'quatro', 'five' => 'seis'])); $page->shouldReceive('supplements')->andReturn(collect(['seven' => 'ocho'])); $page->shouldReceive('title')->andReturn('The Page Title'); + $page->shouldReceive('url')->andReturn('/the-url'); $page->shouldReceive('value')->with('one')->andReturn('dos'); $page->shouldReceive('value')->with('three')->andReturn('quatro'); $page->shouldReceive('value')->with('five')->andReturn('seis'); diff --git a/tests/Tags/StructureTagTest.php b/tests/Tags/StructureTagTest.php index 84b49d9d67e..c54f5debebd 100644 --- a/tests/Tags/StructureTagTest.php +++ b/tests/Tags/StructureTagTest.php @@ -458,6 +458,29 @@ public function it_sets_is_current_and_is_parent_for_a_nav_when_home_is_an_entry #[Test] public function it_uses_the_absolute_url_for_a_nav_entry_link_on_another_site() + { + $this->makeCrossSiteNav(); + + $template = '{{ nav:test }}{{ url }}{{ /nav:test }}'; + + $this->assertEquals('http://two.example.com/projects', (string) Antlers::parse($template, [], true)); + } + + #[Test] + public function it_doesnt_flag_a_nav_entry_link_on_another_site_as_current() + { + $this->makeCrossSiteNav(); + + $mock = \Mockery::mock(\Statamic\Facades\URL::getFacadeRoot())->makePartial(); + \Statamic\Facades\URL::swap($mock); + $mock->shouldReceive('getCurrent')->once()->andReturn('/projects'); + + $template = '{{ nav:test }}{{ if is_current }}current{{ else }}not-current{{ /if }}{{ /nav:test }}'; + + $this->assertEquals('not-current', (string) Antlers::parse($template, [], true)); + } + + private function makeCrossSiteNav() { $this->setSites([ 'en' => ['url' => 'http://one.example.com/', 'locale' => 'en'], @@ -468,26 +491,14 @@ public function it_uses_the_absolute_url_for_a_nav_entry_link_on_another_site() tap(Collection::make('pages')->routes('{slug}'))->sites(['en', 'fr'])->save(); - EntryFactory::id('projects')->collection('pages')->locale('en')->slug('projects')->data(['title' => 'Projects'])->create(); - EntryFactory::id('projects-fr')->origin('projects')->collection('pages')->locale('fr')->slug('projects')->data(['title' => 'Projects'])->create(); + EntryFactory::collection('pages')->id('projects')->locale('en')->slug('projects')->data(['title' => 'Projects'])->create(); + EntryFactory::collection('pages')->id('projects-fr')->origin('projects')->locale('fr')->slug('projects')->data(['title' => 'Projects'])->create(); $nav = Nav::make('test')->canSelectAcrossSites(true); $nav->makeTree('en', [ ['id' => 'link', 'title' => 'Projects', 'entry' => 'projects-fr'], ])->save(); $nav->save(); - - $urlTemplate = '{{ nav:test }}{{ url }}{{ /nav:test }}'; - - $this->assertEquals('http://two.example.com/projects', (string) Antlers::parse($urlTemplate, [], true)); - - $mock = \Mockery::mock(\Statamic\Facades\URL::getFacadeRoot())->makePartial(); - \Statamic\Facades\URL::swap($mock); - $mock->shouldReceive('getCurrent')->andReturn('/projects'); - - $currentTemplate = '{{ nav:test }}{{ if is_current }}current{{ else }}not-current{{ /if }}{{ /nav:test }}'; - - $this->assertEquals('not-current', (string) Antlers::parse($currentTemplate, [], true)); } #[Test] From 30545544a353b2fd80528880deca049dbc7eeb71 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Fri, 21 Aug 2026 20:32:41 +0500 Subject: [PATCH 3/3] Only use the absolute URL for nav entries actually on another site --- src/Structures/Page.php | 12 ++++++++---- tests/Tags/StructureTagTest.php | 11 ++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Structures/Page.php b/src/Structures/Page.php index 1f64ac48fe6..6b372b0eba0 100644 --- a/src/Structures/Page.php +++ b/src/Structures/Page.php @@ -87,7 +87,7 @@ public function url() return null; } - return $this->selectsAcrossSites() ? $entry->absoluteUrl() : $entry->url(); + return $this->linksToAnotherSite($entry) ? $entry->absoluteUrl() : $entry->url(); } public function urlWithoutRedirect() @@ -100,12 +100,16 @@ public function urlWithoutRedirect() return null; } - return $this->selectsAcrossSites() ? $entry->absoluteUrlWithoutRedirect() : $entry->urlWithoutRedirect(); + return $this->linksToAnotherSite($entry) ? $entry->absoluteUrlWithoutRedirect() : $entry->urlWithoutRedirect(); } - private function selectsAcrossSites() + private function linksToAnotherSite(Entry $entry) { - return $this->structure() instanceof Nav && $this->structure()->canSelectAcrossSites(); + if (! $this->structure() instanceof Nav || ! $this->structure()->canSelectAcrossSites()) { + return false; + } + + return $entry->site()->handle() !== $this->tree->site()->handle(); } public function isRedirect() diff --git a/tests/Tags/StructureTagTest.php b/tests/Tags/StructureTagTest.php index c54f5debebd..747540a1211 100644 --- a/tests/Tags/StructureTagTest.php +++ b/tests/Tags/StructureTagTest.php @@ -461,13 +461,13 @@ public function it_uses_the_absolute_url_for_a_nav_entry_link_on_another_site() { $this->makeCrossSiteNav(); - $template = '{{ nav:test }}{{ url }}{{ /nav:test }}'; + $template = '{{ nav:test }}[{{ id }}={{ url }}]{{ /nav:test }}'; - $this->assertEquals('http://two.example.com/projects', (string) Antlers::parse($template, [], true)); + $this->assertEquals('[link=http://two.example.com/projects][local-link=/projects]', (string) Antlers::parse($template, [], true)); } #[Test] - public function it_doesnt_flag_a_nav_entry_link_on_another_site_as_current() + public function it_only_flags_the_local_nav_entry_link_as_current() { $this->makeCrossSiteNav(); @@ -475,9 +475,9 @@ public function it_doesnt_flag_a_nav_entry_link_on_another_site_as_current() \Statamic\Facades\URL::swap($mock); $mock->shouldReceive('getCurrent')->once()->andReturn('/projects'); - $template = '{{ nav:test }}{{ if is_current }}current{{ else }}not-current{{ /if }}{{ /nav:test }}'; + $template = '{{ nav:test }}[{{ id }}{{ if is_current }}=current{{ /if }}]{{ /nav:test }}'; - $this->assertEquals('not-current', (string) Antlers::parse($template, [], true)); + $this->assertEquals('[link][local-link=current]', (string) Antlers::parse($template, [], true)); } private function makeCrossSiteNav() @@ -497,6 +497,7 @@ private function makeCrossSiteNav() $nav = Nav::make('test')->canSelectAcrossSites(true); $nav->makeTree('en', [ ['id' => 'link', 'title' => 'Projects', 'entry' => 'projects-fr'], + ['id' => 'local-link', 'title' => 'Projects (local)', 'entry' => 'projects'], ])->save(); $nav->save(); }