From f2586a29046bfeee4203a79a5a634cadc1fe2cfb Mon Sep 17 00:00:00 2001 From: Arda-dd Date: Sat, 24 Oct 2026 13:21:17 +0300 Subject: [PATCH] Resolve entry links in nav page urls --- .../Navigation/NavigationPagesController.php | 2 +- src/Structures/Page.php | 39 ++++++++++++++-- tests/Data/Structures/PageTest.php | 44 +++++++++++++++++++ .../Navigation/EditNavigationPageTest.php | 28 ++++++++++++ 4 files changed, 109 insertions(+), 4 deletions(-) diff --git a/src/Http/Controllers/CP/Navigation/NavigationPagesController.php b/src/Http/Controllers/CP/Navigation/NavigationPagesController.php index 0c5e7199ff8..b3b78fb283d 100644 --- a/src/Http/Controllers/CP/Navigation/NavigationPagesController.php +++ b/src/Http/Controllers/CP/Navigation/NavigationPagesController.php @@ -126,7 +126,7 @@ private function getPageValues($page) ->merge($page->pageData()) ->merge([ 'title' => $page->title(), - 'url' => $page->reference() ? null : $page->url(), + 'url' => $page->reference() ? null : $page->customUrl(), ])->all(); } diff --git a/src/Structures/Page.php b/src/Structures/Page.php index 6b372b0eba0..4164de21916 100644 --- a/src/Structures/Page.php +++ b/src/Structures/Page.php @@ -3,6 +3,7 @@ namespace Statamic\Structures; use ArrayAccess; +use Facades\Statamic\Routing\ResolveRedirect; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Responsable; use Illuminate\Support\Traits\ForwardsCalls; @@ -80,7 +81,7 @@ public function setUrl($url) public function url() { if ($this->url) { - return $this->url; + return $this->resolveCustomUrl(); } if (! $entry = $this->entry()) { @@ -93,7 +94,7 @@ public function url() public function urlWithoutRedirect() { if ($this->url) { - return $this->url; + return $this->resolveCustomUrl(); } if (! $entry = $this->entry()) { @@ -103,6 +104,36 @@ public function urlWithoutRedirect() return $this->linksToAnotherSite($entry) ? $entry->absoluteUrlWithoutRedirect() : $entry->urlWithoutRedirect(); } + public function customUrl() + { + return $this->url; + } + + /** + * When the navigation blueprint uses a link field for the url, the custom url + * can be a reference like "entry::id" or "asset::container::path". + */ + private function resolveCustomUrl() + { + if (! Str::contains($this->url, '::')) { + return $this->url; + } + + $canSelectAcrossSites = $this->structure() instanceof Nav && $this->structure()->canSelectAcrossSites(); + + $item = ResolveRedirect::item($this->url, $this, ! $canSelectAcrossSites); + + if (! is_object($item)) { + return $item; + } + + if ($item instanceof Entry && $this->linksToAnotherSite($item)) { + return $item->absoluteUrl(); + } + + return $item->url(); + } + private function linksToAnotherSite(Entry $entry) { if (! $this->structure() instanceof Nav || ! $this->structure()->canSelectAcrossSites()) { @@ -272,7 +303,9 @@ public function absoluteUrl() } if ($this->url) { - return $this->absoluteUrl = URL::makeAbsolute($this->url); + $url = $this->resolveCustomUrl(); + + return $this->absoluteUrl = $url ? URL::makeAbsolute($url) : null; } return $this->absoluteUrl = optional($this->entry())->absoluteUrl(); diff --git a/tests/Data/Structures/PageTest.php b/tests/Data/Structures/PageTest.php index 63cd7d1b2ca..37f172962c0 100644 --- a/tests/Data/Structures/PageTest.php +++ b/tests/Data/Structures/PageTest.php @@ -328,6 +328,50 @@ public function it_gets_the_uri_of_a_hardcoded_absolute_link() $this->assertTrue($page->hasCustomUrl()); } + #[Test] + public function it_resolves_a_hardcoded_link_to_an_entry() + { + // The navigation blueprint can use a link field for the url, which stores entry references. + Collections::make('pages')->routes('{slug}')->save(); + EntryFactory::collection('pages')->id('123')->slug('contact')->create(); + + $nav = $this->mock(Nav::class); + $nav->shouldReceive('canSelectAcrossSites')->andReturnFalse(); + + $tree = $this->newTree()->setStructure($nav); + + $page = (new Page) + ->setTree($tree) + ->setUrl('entry::123'); + + $this->assertEquals('/contact', $page->uri()); + $this->assertEquals('/contact', $page->url()); + $this->assertEquals('/contact', $page->urlWithoutRedirect()); + $this->assertEquals('http://localhost/contact', $page->absoluteUrl()); + $this->assertEquals('http://localhost/contact', $page->absoluteUrlWithoutRedirect()); + $this->assertEquals('entry::123', $page->customUrl()); + $this->assertTrue($page->hasCustomUrl()); + } + + #[Test] + public function a_hardcoded_link_to_a_missing_entry_has_no_url() + { + $nav = $this->mock(Nav::class); + $nav->shouldReceive('canSelectAcrossSites')->andReturnFalse(); + + $tree = $this->newTree()->setStructure($nav); + + $page = (new Page) + ->setTree($tree) + ->setUrl('entry::unknown'); + + $this->assertNull($page->uri()); + $this->assertNull($page->url()); + $this->assertNull($page->urlWithoutRedirect()); + $this->assertNull($page->absoluteUrl()); + $this->assertNull($page->absoluteUrlWithoutRedirect()); + } + #[Test] public function it_gets_the_uri_of_a_hardcoded_text_only_page() { diff --git a/tests/Feature/Navigation/EditNavigationPageTest.php b/tests/Feature/Navigation/EditNavigationPageTest.php index f7dacbfac53..af2b6a1a4ff 100644 --- a/tests/Feature/Navigation/EditNavigationPageTest.php +++ b/tests/Feature/Navigation/EditNavigationPageTest.php @@ -94,6 +94,34 @@ public function it_gets_the_values_for_a_regular_nav_item() ]); } + #[Test] + public function it_gets_the_stored_value_of_a_link_to_an_entry() + { + $this->withoutExceptionHandling(); + $this->mockTextFieldtype(); + $user = tap(User::make()->makeSuper())->save(); + $nav = tap(Nav::make('test'))->save(); + $nav->makeTree('en', [ + [ + 'id' => 'id7', + 'title' => 'The title', + 'url' => 'entry::123', + ], + ])->save(); + BlueprintRepository::partialMock(); + BlueprintRepository::shouldReceive('find')->with('navigation.test')->andReturn(Blueprint::makeFromFields([])); + + // The form gets the reference, not the url it resolves to, so that saving keeps the link. + $this + ->actingAs($user) + ->request($nav, 'id7') + ->assertJson([ + 'values' => [ + 'url' => 'entry::123 (preprocessed)', + ], + ]); + } + #[Test] public function it_gets_the_values_for_a_text_only_nav_item() {