From b5e1995cacadb1ce8c147ee047455c1664810929 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 16 Oct 2026 21:34:58 +0200 Subject: [PATCH] fix(services): link Website button to project page, not host root Closes #11752 --- app/Livewire/Project/New/Select.php | 24 +++++++++++++++++ .../livewire/project/new/select.blade.php | 12 +-------- .../ServiceTemplatesLastUpdatedHintTest.php | 27 +++++++++++++++++++ 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php index 629a7e56d9..656a5710ca 100644 --- a/app/Livewire/Project/New/Select.php +++ b/app/Livewire/Project/New/Select.php @@ -118,6 +118,7 @@ class Select extends Component 'name' => str($serviceKey)->headline(), 'docsSlug' => str($serviceKey)->lower()->value(), 'templateLastUpdated' => $templateLastUpdatedMap[$serviceKey] ?? null, + 'website' => $this->serviceWebsiteUrl(data_get($service, 'documentation')), ] + service_logo_urls(data_get($service, 'logo')) + (array) $service; })->all(); @@ -288,6 +289,29 @@ class Select extends Component } } + /** + * Derive a website link from the documentation URL. Shared hosts keep the + * path that identifies the project, so the link does not open the host root. + */ + private function serviceWebsiteUrl(?string $documentation): ?string + { + $url = parse_url((string) $documentation); + if (! in_array($url['scheme'] ?? null, ['http', 'https'], true) || blank($url['host'] ?? null)) { + return null; + } + + $projectPathSegments = [ + 'github.com' => 2, + 'gitlab.com' => 2, + 'codeberg.org' => 2, + 'hub.docker.com' => 3, + ][str($url['host'])->lower()->chopStart('www.')->value()] ?? 0; + + $projectPath = str($url['path'] ?? '')->explode('/')->filter()->take($projectPathSegments)->implode('/'); + + return rtrim("{$url['scheme']}://{$url['host']}/{$projectPath}", '/'); + } + private function serviceTemplatesLastUpdated(): ?string { $fetchedAt = get_service_templates_fetched_at(); diff --git a/resources/views/livewire/project/new/select.blade.php b/resources/views/livewire/project/new/select.blade.php index 7722722582..1e33c43d8a 100644 --- a/resources/views/livewire/project/new/select.blade.php +++ b/resources/views/livewire/project/new/select.blade.php @@ -317,7 +317,7 @@ class="button" :class="{ 'opacity-60': docCheckInProgress[service.name] }"> Docs - Website @@ -471,16 +471,6 @@ databaseWebsiteUrl(database) { return this.databaseLinks[database.id]?.website; }, - serviceWebsiteUrl(service) { - const source = service.website || service.homepage || service.documentation; - if (!source) return null; - - try { - return new URL(source).origin; - } catch (error) { - return null; - } - }, async checkUrlExists(url) { if (!url) return false; try { diff --git a/tests/Feature/ServiceTemplatesLastUpdatedHintTest.php b/tests/Feature/ServiceTemplatesLastUpdatedHintTest.php index 7619b81e19..105ab6db68 100644 --- a/tests/Feature/ServiceTemplatesLastUpdatedHintTest.php +++ b/tests/Feature/ServiceTemplatesLastUpdatedHintTest.php @@ -135,3 +135,30 @@ it('preserves one click service key casing when selecting a service template', f expect($component->type)->toBe('one-click-service-denoKV'); }); + +it('links service websites to the project page instead of a shared host root', function () { + $path = base_path('templates/'.config('constants.services.file_name')); + $payload = json_encode([ + 'uptime-kuma' => ['documentation' => 'https://github.com/louislam/uptime-kuma?tab=readme-ov-file&utm_source=coolify.io'], + 'alexandrie' => ['documentation' => 'https://www.github.com/Smaug6739/Alexandrie/tree/main/docs'], + 'radarr' => ['documentation' => 'https://hub.docker.com/r/linuxserver/radarr?utm_source=coolify.io'], + 'n8n' => ['documentation' => 'https://n8n.io/docs?utm_source=coolify.io'], + 'broken' => ['documentation' => 'not a url'], + ]); + + File::partialMock() + ->shouldReceive('exists') + ->with($path) + ->andReturn(true) + ->shouldReceive('get') + ->with($path) + ->andReturn($payload); + + $services = (new Select)->loadServices()['services']; + + expect($services['uptime-kuma']['website'])->toBe('https://github.com/louislam/uptime-kuma') + ->and($services['alexandrie']['website'])->toBe('https://www.github.com/Smaug6739/Alexandrie') + ->and($services['radarr']['website'])->toBe('https://hub.docker.com/r/linuxserver/radarr') + ->and($services['n8n']['website'])->toBe('https://n8n.io') + ->and($services['broken']['website'])->toBeNull(); +});