From 5f20cb3063b8876abe0edd668181283a1262e311 Mon Sep 17 00:00:00 2001 From: Daniel Haven <49914607+danielh-official@users.noreply.github.com> Date: Wed, 1 Apr 2026 08:15:18 -0400 Subject: [PATCH] Add canonical links for documentation pages --- .../ShowDocumentationController.php | 9 ++ app/Services/DocsVersionService.php | 40 +++++++ tests/Unit/DocsVersionServiceTest.php | 108 ++++++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 app/Services/DocsVersionService.php create mode 100644 tests/Unit/DocsVersionServiceTest.php diff --git a/app/Http/Controllers/ShowDocumentationController.php b/app/Http/Controllers/ShowDocumentationController.php index 8dda7bb3..99166c45 100644 --- a/app/Http/Controllers/ShowDocumentationController.php +++ b/app/Http/Controllers/ShowDocumentationController.php @@ -2,7 +2,9 @@ namespace App\Http\Controllers; +use App\Services\DocsVersionService; use App\Support\CommonMark\CommonMark; +use Artesaos\SEOTools\Facades\SEOMeta; use Artesaos\SEOTools\Facades\SEOTools; use Illuminate\Contracts\View\Factory as ViewFactory; use Illuminate\Http\RedirectResponse; @@ -49,6 +51,13 @@ public function __invoke(Request $request, string $platform, string $version, ?s $title = $pageProperties['title'].' - NativePHP '.$platform.' v'.$version; $description = Arr::exists($pageProperties, 'description') ? $pageProperties['description'] : 'NativePHP documentation for '.$platform.' v'.$version; + $canonicalUrl = app(DocsVersionService::class)->determineCanonicalUrl( + platform: $platform, + page: $page, + ); + + SEOMeta::setCanonical($canonicalUrl); + SEOTools::setTitle($title); SEOTools::setDescription($description); diff --git a/app/Services/DocsVersionService.php b/app/Services/DocsVersionService.php new file mode 100644 index 00000000..474ac8a1 --- /dev/null +++ b/app/Services/DocsVersionService.php @@ -0,0 +1,40 @@ +resolveOldVersionApisWithPluginsCorePage($platform, $latestVersion, $page); + + $latestPagePath = resource_path("views/docs/{$platform}/{$latestVersion}/{$page}.md"); + + $page = file_exists($latestPagePath) ? $page : 'getting-started/introduction'; + + return route('docs.show', [ + 'platform' => $platform, + 'version' => $latestVersion, + 'page' => $page, + ]); + } + + /** + * Handle renamed paths (e.g., apis/* moved to plugins/core/*) + */ + public function resolveOldVersionApisWithPluginsCorePage(string $platform, string $latestVersion, string $page): string + { + if (str_starts_with($page, 'apis/')) { + $remappedPage = 'plugins/core/'.substr($page, 5); + $remappedPath = resource_path("views/docs/{$platform}/{$latestVersion}/{$remappedPage}.md"); + + if (file_exists($remappedPath)) { + $page = $remappedPage; + } + } + + return $page; + } +} diff --git a/tests/Unit/DocsVersionServiceTest.php b/tests/Unit/DocsVersionServiceTest.php new file mode 100644 index 00000000..bd010d3b --- /dev/null +++ b/tests/Unit/DocsVersionServiceTest.php @@ -0,0 +1,108 @@ +docsVersionService = app(DocsVersionService::class); + } + + public static function platformDataProvider(): array + { + return [ + 'platform=mobile' => ['mobile'], + 'platform=desktop' => ['desktop'], + ]; + } + + #[Test] + #[DataProvider('platformDataProvider')] + public function it_points_to_the_latest_version_when_page_exists_in_latest( + string $platform, + ): void { + $latestVersion = $platform === 'mobile' ? config('docs.latest_versions.mobile') : config('docs.latest_versions.desktop'); + + $url = $this->docsVersionService->determineCanonicalUrl( + platform: $platform, + page: 'getting-started/introduction', + ); + + $expected = route('docs.show', [ + 'platform' => $platform, + 'version' => $latestVersion, + 'page' => 'getting-started/introduction', + ]); + + $this->assertEquals($expected, $url); + } + + #[Test] + public function it_remaps_mobile_apis_pages_to_plugins_core_when_the_page_exists_there_in_latest(): void + { + $url = $this->docsVersionService->determineCanonicalUrl( + platform: 'mobile', + page: 'apis/camera', + ); + + $expected = route('docs.show', [ + 'platform' => 'mobile', + 'version' => config('docs.latest_versions.mobile'), + 'page' => 'plugins/core/camera', + ]); + + $this->assertEquals($expected, $url); + } + + #[Test] + public function it_points_to_the_latest_version_of_getting_started_introduction_when_page_does_not_exist_in_latest(): void + { + // concepts/ci-cd only exists in version 1 of mobile documentation + $url = $this->docsVersionService->determineCanonicalUrl( + platform: 'mobile', + page: 'concepts/ci-cd', + ); + + $expected = route('docs.show', [ + 'platform' => 'mobile', + 'version' => '3', + 'page' => 'getting-started/introduction', + ]); + + $this->assertEquals($expected, $url); + } + + #[Test] + #[DataProvider('platformDataProvider')] + public function it_points_to_the_latest_version_of_getting_started_introduction_when_page_does_not_exist( + string $platform, + ): void { + $latestVersion = $platform === 'mobile' ? config('docs.latest_versions.mobile') : config('docs.latest_versions.desktop'); + + $url = $this->docsVersionService->determineCanonicalUrl( + platform: $platform, + page: 'non-existent-page', + ); + + $expected = route('docs.show', [ + 'platform' => $platform, + 'version' => $latestVersion, + 'page' => 'getting-started/introduction', + ]); + + $this->assertEquals($expected, $url); + } +}