diff --git a/CHANGELOG.md b/CHANGELOG.md
index 991e725e6df..22a6bd714a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,6 +36,7 @@ This serves two purposes:
- Updated the scroll to top button to smooth scroll to the top of the page in https://github.com/hydephp/develop/pull/2458
- Fix build command trying to use Vite in site builds if server is running in https://github.com/hydephp/develop/issues/2483
- Fixed missing content type headers for JSON and XML in the realtime compiler in https://github.com/hydephp/develop/pull/2496
+- Fixed dashboard links not resolving properly when there is a trailing slash in the URL in https://github.com/hydephp/develop/pull/2499
### Security
- in case of vulnerabilities.
diff --git a/packages/realtime-compiler/resources/dashboard.blade.php b/packages/realtime-compiler/resources/dashboard.blade.php
index d7bcf0fb77a..de3f37a2c52 100644
--- a/packages/realtime-compiler/resources/dashboard.blade.php
+++ b/packages/realtime-compiler/resources/dashboard.blade.php
@@ -268,7 +268,7 @@
@endif
@endif
- View
+ View
@@ -300,7 +300,7 @@
@if($dashboard->isInteractive())
diff --git a/packages/realtime-compiler/src/Http/DashboardController.php b/packages/realtime-compiler/src/Http/DashboardController.php
index fd74f487dd2..fdd3620848b 100644
--- a/packages/realtime-compiler/src/Http/DashboardController.php
+++ b/packages/realtime-compiler/src/Http/DashboardController.php
@@ -22,6 +22,7 @@
use Desilva\Microserve\JsonResponse;
use Desilva\Microserve\HtmlResponse;
use Hyde\Support\Filesystem\MediaFile;
+use Hyde\Support\Models\Route;
use Illuminate\Support\Facades\Process;
use Hyde\Framework\Actions\StaticPageBuilder;
use Hyde\Framework\Actions\AnonymousViewCompiler;
@@ -135,6 +136,31 @@ public function getPageList(): array
return Hyde::routes()->all();
}
+ public function getRoutePreviewLink(Route $route): string
+ {
+ return $this->rootRelativeLink($route->getLink());
+ }
+
+ public function getMediaPreviewLink(MediaFile $mediaFile): string
+ {
+ return $this->rootRelativeLink('media/'.$mediaFile->getIdentifier());
+ }
+
+ protected function rootRelativeLink(string $link): string
+ {
+ if (str_starts_with($link, '#') || preg_match('/^[a-z][a-z0-9+.-]*:/i', $link) || str_starts_with($link, '//')) {
+ return $link;
+ }
+
+ if ($link === './') {
+ return '/';
+ }
+
+ $link = preg_replace('#^(\.\./)+#', '', $link) ?? $link;
+
+ return '/'.ltrim($link, '/');
+ }
+
/** @internal */
public static function bytesToHuman(int $bytes, int $precision = 2): string
{
diff --git a/packages/realtime-compiler/tests/RealtimeCompilerTest.php b/packages/realtime-compiler/tests/RealtimeCompilerTest.php
index 01b6e81d8fc..5d01a1bb040 100644
--- a/packages/realtime-compiler/tests/RealtimeCompilerTest.php
+++ b/packages/realtime-compiler/tests/RealtimeCompilerTest.php
@@ -268,6 +268,33 @@ public function testPingRouteReturnsPingResponse()
$this->assertSame('OK', $response->statusMessage);
}
+ public function testDashboardLinksAreRootRelativeWhenAccessedWithTrailingSlash()
+ {
+ $dashboardEnvironment = getenv('SERVER_DASHBOARD');
+ putenv('SERVER_DASHBOARD=true');
+ $this->mockCompilerRoute('dashboard/');
+
+ Filesystem::put('_pages/foo.md', '# Hello World!');
+ Filesystem::put('_media/test.css', 'body {}');
+
+ try {
+ $kernel = new HttpKernel();
+ $response = $kernel->handle(new Request());
+
+ $this->assertInstanceOf(HtmlResponse::class, $response);
+ $this->assertSame(200, $response->statusCode);
+ $this->assertSame('OK', $response->statusMessage);
+ $this->assertStringContainsString('href="/foo.html"', $response->body);
+ $this->assertStringContainsString('href="/media/test.css"', $response->body);
+ $this->assertStringNotContainsString('href="foo.html"', $response->body);
+ $this->assertStringNotContainsString('href="media/test.css"', $response->body);
+ } finally {
+ Filesystem::unlink('_pages/foo.md');
+ Filesystem::unlink('_media/test.css');
+ putenv($dashboardEnvironment === false ? 'SERVER_DASHBOARD' : "SERVER_DASHBOARD=$dashboardEnvironment");
+ }
+ }
+
public function testExceptionHandling()
{
$exception = new Exception('foo');