Skip to content

Commit c8b9259

Browse files
committed
refactor: immutable urls with canonical/norobot
1 parent 6f85107 commit c8b9259

File tree

9 files changed

+113
-82
lines changed

9 files changed

+113
-82
lines changed

bun.lock

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,22 @@
44
"": {
55
"dependencies": {
66
"highlight.js": "^11.11.1",
7-
"puppeteer": "24.15.0",
7+
"puppeteer": "^24.11.2",
88
},
99
"devDependencies": {
1010
"@fontsource-variable/kantumruy-pro": "^5.2.6",
1111
"@fontsource-variable/public-sans": "^5.2.6",
1212
"@tailwindcss/typography": "^0.5.16",
1313
"@tailwindcss/vite": "^4.1.11",
14-
"@vitejs/plugin-vue": "6.0.1",
15-
"@vueuse/core": "13.6.0",
14+
"@vitejs/plugin-vue": "^6.0.0",
15+
"@vueuse/core": "^13.5.0",
1616
"fuse.js": "^7.1.0",
17-
"reka-ui": "2.4.1",
17+
"reka-ui": "^2.3.2",
1818
"tailwindcss": "^4.1.11",
1919
"typescript": "^5.8.3",
2020
"vite": "^6.3.5",
2121
"vite-plugin-tempest": "^0.0.2",
22-
"vue": "3.5.18",
22+
"vue": "^3.5.17",
2323
},
2424
},
2525
},

src/Web/Documentation/Chapter.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,22 @@ public function __construct(
2323

2424
public function getUri(): string
2525
{
26-
return uri(DocumentationController::class, version: $this->version, category: $this->category, slug: $this->slug);
26+
return uri(
27+
DocumentationController::class,
28+
version: $this->version,
29+
category: $this->category,
30+
slug: $this->slug,
31+
);
32+
}
33+
34+
public function getCanonicalUri(): string
35+
{
36+
return uri(
37+
DocumentationController::class,
38+
version: $this->version->default(),
39+
category: $this->category,
40+
slug: $this->slug,
41+
);
2742
}
2843

2944
public function getMetaUri(): string

src/Web/Documentation/ChapterRepository.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
use App\Support\HasMemoization;
88
use League\CommonMark\Extension\FrontMatter\Output\RenderedContentWithFrontMatter;
99
use League\CommonMark\MarkdownConverter;
10+
use RuntimeException;
1011
use Spatie\YamlFrontMatter\YamlFrontMatter;
12+
use Tempest\Http\HttpRequestFailed;
13+
use Tempest\Http\Status;
1114
use Tempest\Support\Arr\ImmutableArray;
1215

1316
use function Tempest\root_path;
@@ -29,8 +32,14 @@ public function find(Version $version, string $category, string $slug): ?Chapter
2932
{
3033
$category = replace($category, '/^\d+-/', '');
3134
$slug = replace($slug, '/^\d+-/', '');
35+
$directory = __DIR__ . "/content/{$version->getUrlSegment()}";
36+
37+
if (! is_dir($directory)) {
38+
throw new RuntimeException("Documentation for version {$version->value} has not been fetched. Run `tempest docs:pull {$version->value}`.");
39+
}
40+
3241
$path = ImmutableArray::createFrom(recursive_search(
33-
folder: __DIR__ . "/content/{$version->value}",
42+
folder: $directory,
3443
pattern: sprintf("#.*\/(\d+-)?%s\/(\d+-)?%s\.md#", preg_quote($category, '#'), preg_quote($slug, '#')),
3544
))->sort()->first();
3645

@@ -68,7 +77,7 @@ public function all(Version $version, string $category = '*'): ImmutableArray
6877
{
6978
return $this->memoize(
7079
$version->value . $category,
71-
fn () => arr(glob(__DIR__ . "/content/{$version->value}/*{$category}/*.md"))
80+
fn () => arr(glob(__DIR__ . "/content/{$version->getUrlSegment()}/*{$category}/*.md"))
7281
->map(function (string $path) use ($version) {
7382
$content = file_get_contents($path);
7483
$category = str($path)->beforeLast('/')->afterLast('/')->replaceRegex('/^\d+-/', '');

src/Web/Documentation/ChapterView.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public function previousChapter(): ?Chapter
118118
public function categories(): array
119119
{
120120
return map_iterable(
121-
array: glob(__DIR__ . "/content/{$this->version->value}/*", flags: GLOB_ONLYDIR),
121+
array: glob(__DIR__ . "/content/{$this->version->getUrlSegment()}/*", flags: GLOB_ONLYDIR),
122122
map: fn (string $path) => str($path)->afterLast('/')->replaceRegex('/^\d+-/', '')->toString(),
123123
);
124124
}

src/Web/Documentation/DefaultDocumentationDataProvider.php

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/Web/Documentation/DocumentationController.php

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Tempest\Http\Responses\Redirect;
1010
use Tempest\Router\Get;
1111
use Tempest\Router\StaticPage;
12+
use Tempest\Support\Arr\ImmutableArray;
1213
use Tempest\Support\Str\ImmutableString;
1314
use Tempest\View\View;
1415

@@ -20,58 +21,48 @@
2021
{
2122
#[Get('/current/{path:.*}')]
2223
#[Get('/main/{path:.*}')]
23-
public function docsRedirect(string $path): Redirect
24+
#[Get('/docs/{path:.*}')]
25+
public function redirect(string $path): Redirect
2426
{
25-
return new Redirect(sprintf('/%s/%s', Version::default()->value, $path));
27+
return new Redirect(sprintf('/%s/%s', Version::default()->getUrlSegment(), $path));
2628
}
2729

28-
#[Get('/docs')]
2930
#[Get('/documentation')]
30-
#[Get('/main/framework/getting-started')]
3131
public function index(): Redirect
3232
{
3333
$version = Version::default();
3434

35-
$category = arr(glob(__DIR__ . "/content/{$version->value}/*", flags: GLOB_ONLYDIR))
35+
$category = arr(glob(__DIR__ . "/content/{$version->getUrlSegment()}/*", flags: GLOB_ONLYDIR))
36+
->tap(fn (ImmutableArray $files) => $files->isEmpty() ? throw new \RuntimeException('Documentation has not been fetched. Run `tempest docs:pull`.') : null)
3637
->sort()
3738
->mapFirstTo(ImmutableString::class)
3839
->basename()
3940
->toString();
4041

41-
$slug = arr(glob(__DIR__ . "/content/{$version->value}/{$category}/*.md"))
42+
$slug = arr(glob(__DIR__ . "/content/{$version->getUrlSegment()}/{$category}/*.md"))
4243
->map(fn (string $path) => before_first(basename($path), '.'))
4344
->sort()
4445
->mapFirstTo(ImmutableString::class)
4546
->basename()
4647
->toString();
4748

4849
return new Redirect(uri(
49-
[self::class, 'default'],
50+
[self::class, '__invoke'],
51+
version: $version,
5052
category: str_replace('0-', '', $category),
5153
slug: str_replace('01-', '', $slug),
5254
));
5355
}
5456

55-
#[StaticPage(DefaultDocumentationDataProvider::class)]
56-
#[Get('/docs/{category}/{slug}')]
57-
public function default(string $category, string $slug, ChapterRepository $chapterRepository): View|Response
58-
{
59-
return $this->chapterView(Version::default(), $category, $slug, $chapterRepository) ?? new NotFound();
60-
}
61-
6257
#[StaticPage(DocumentationDataProvider::class)]
6358
#[Get('/{version}/{category}/{slug}')]
6459
public function __invoke(string $version, string $category, string $slug, ChapterRepository $chapterRepository): View|Response
6560
{
66-
if ($version === Version::default()->value) {
67-
return new Redirect(uri([self::class, 'default'], category: $category, slug: $slug));
68-
}
69-
7061
if (is_null($version = Version::tryFromString($version))) {
7162
return new NotFound();
7263
}
7364

74-
return $this->chapterView($version, $category, $slug, $chapterRepository) ?? new NotFound();
65+
return $this->chapterView($version, $category, $slug, $chapterRepository) ?? new Redirect(uri(self::class, 'index'));
7566
}
7667

7768
private function chapterView(Version $version, string $category, string $slug, ChapterRepository $chapterRepository): ?ChapterView

src/Web/Documentation/RedirectMiddleware.php

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Tempest\Core\Priority;
66
use Tempest\Http\Request;
77
use Tempest\Http\Response;
8+
use Tempest\Http\Responses\NotFound;
89
use Tempest\Http\Responses\Redirect;
910
use Tempest\Router\HttpMiddleware;
1011
use Tempest\Router\HttpMiddlewareCallable;
@@ -15,35 +16,41 @@
1516
use function Tempest\Support\Arr\get_by_key;
1617
use function Tempest\Support\Regex\matches;
1718
use function Tempest\Support\str;
19+
use function Tempest\uri;
1820

1921
#[Priority(Priority::HIGHEST)]
2022
final readonly class RedirectMiddleware implements HttpMiddleware
2123
{
2224
public function __construct(
2325
private Router $router,
26+
private MatchedRoute $route,
2427
) {}
2528

2629
#[\Override]
2730
public function __invoke(Request $request, HttpMiddlewareCallable $next): Response
2831
{
2932
$path = str($request->path);
3033
$response = $next($request);
31-
$matched = get(MatchedRoute::class);
34+
$version = Version::tryFromString(get_by_key($this->route->params, 'version'));
3235

3336
// If not a docs page, let's just continue normal flow
34-
if ($matched->route->uri !== '/{version}/{category}/{slug}') {
37+
if ($this->route->route->uri !== '/{version}/{category}/{slug}') {
3538
return $response;
3639
}
3740

3841
// Redirect to slugs without numbers
39-
if (matches($matched->params['category'], '/^\d+-/') || matches($matched->params['slug'], '/^\d+-/')) {
42+
if (matches($this->route->params['category'], '/^\d+-/') || matches($this->route->params['slug'], '/^\d+-/')) {
4043
return new Redirect($path->replaceRegex('/\/\d+-/', '/'));
4144
}
4245

46+
// If no version found, 404
47+
if ($version === null) {
48+
return new Redirect(uri([DocumentationController::class, 'index']));
49+
}
50+
4351
// Redirect to actual version
44-
$version = Version::tryFromString(get_by_key($matched->params, 'version'));
45-
if ($version->value !== $matched->params['version']) {
46-
return new Redirect($path->replace("/{$matched->params['version']}/", "/{$version->value}/"));
52+
if ($version->getUrlSegment() !== $this->route->params['version']) {
53+
return new Redirect($path->replace("/{$this->route->params['version']}/", "/{$version->getUrlSegment()}/"));
4754
}
4855

4956
return $response;

src/Web/Documentation/Version.php

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,25 @@ enum Version: string
88
{
99
use IsEnumHelper;
1010

11-
case VERSION_1 = 'main';
11+
case VERSION_1 = '1.x';
1212
case VERSION_2 = '2.x';
1313

14+
public function isNext(): bool
15+
{
16+
return match ($this) {
17+
self::VERSION_2 => true,
18+
default => false,
19+
};
20+
}
21+
22+
public function isCurrent(): bool
23+
{
24+
return match ($this) {
25+
self::VERSION_1 => true,
26+
default => false,
27+
};
28+
}
29+
1430
public function getBranch(): string
1531
{
1632
return match ($this) {
@@ -22,15 +38,21 @@ public function getBranch(): string
2238
public function getUrlSegment(): string
2339
{
2440
return match ($this) {
25-
self::VERSION_1 => 'main',
41+
self::VERSION_1 => '1.x',
2642
self::VERSION_2 => '2.x',
2743
};
2844
}
2945

46+
public function isPrevious(): bool
47+
{
48+
return ! $this->isNext() && ! $this->isCurrent();
49+
}
50+
3051
public static function tryFromString(?string $case): ?static
3152
{
3253
return match ($case) {
33-
'default', 'current', null => self::default(),
54+
'default', 'current', 'main', null => self::default(),
55+
'next' => self::VERSION_2,
3456
default => self::tryFrom($case),
3557
};
3658
}

0 commit comments

Comments
 (0)