radiant-docs 0.1.37 → 0.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/astro.config.mjs +2 -0
- package/template/src/components/Footer.astro +1 -1
- package/template/src/components/Header.astro +8 -8
- package/template/src/components/OpenApiPage.astro +18 -18
- package/template/src/components/Search.astro +18 -18
- package/template/src/components/Sidebar.astro +4 -2
- package/template/src/components/SidebarDropdown.astro +82 -79
- package/template/src/components/SidebarSegmented.astro +5 -5
- package/template/src/components/TableOfContents.astro +24 -15
- package/template/src/components/ThemeSwitcher.astro +15 -8
- package/template/src/components/chat/AskAiWidget.tsx +4 -3
- package/template/src/components/endpoint/PlaygroundBar.astro +3 -3
- package/template/src/components/endpoint/PlaygroundButton.astro +2 -2
- package/template/src/components/endpoint/PlaygroundField.astro +53 -53
- package/template/src/components/endpoint/PlaygroundForm.astro +38 -22
- package/template/src/components/endpoint/RequestSnippets.astro +54 -21
- package/template/src/components/endpoint/ResponseDisplay.astro +24 -24
- package/template/src/components/endpoint/ResponseFieldTree.astro +12 -12
- package/template/src/components/endpoint/ResponseFields.astro +19 -19
- package/template/src/components/endpoint/ResponseSnippets.astro +66 -29
- package/template/src/components/ui/CodeTabEdge.astro +6 -4
- package/template/src/components/ui/Field.astro +7 -7
- package/template/src/components/ui/demo/Demo.astro +1 -1
- package/template/src/components/user/Accordion.astro +3 -3
- package/template/src/components/user/Callout.astro +8 -8
- package/template/src/components/user/CodeBlock.astro +63 -25
- package/template/src/components/user/CodeGroup.astro +259 -22
- package/template/src/components/user/ComponentPreviewBlock.astro +36 -10
- package/template/src/components/user/Image.astro +2 -2
- package/template/src/components/user/Step.astro +4 -4
- package/template/src/components/user/Tab.astro +1 -1
- package/template/src/components/user/Tabs.astro +142 -42
- package/template/src/layouts/Layout.astro +1 -1
- package/template/src/lib/code/code-block.ts +150 -15
- package/template/src/lib/mdx/remark-resolve-internal-links.ts +639 -0
- package/template/src/pages/404.astro +44 -0
- package/template/src/styles/global.css +51 -19
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { Link, Root } from "mdast";
|
|
3
|
+
import type { Plugin } from "unified";
|
|
4
|
+
import { visitParents } from "unist-util-visit-parents";
|
|
5
|
+
import {
|
|
6
|
+
getConfig,
|
|
7
|
+
loadOpenApiSpec,
|
|
8
|
+
type DocsConfig,
|
|
9
|
+
type NavGroup,
|
|
10
|
+
type NavMenuItem,
|
|
11
|
+
type NavOpenApi,
|
|
12
|
+
type NavOpenApiPage,
|
|
13
|
+
type NavPage,
|
|
14
|
+
} from "../validation";
|
|
15
|
+
import {
|
|
16
|
+
buildMdxPageHref,
|
|
17
|
+
buildOpenApiEndpointSlug,
|
|
18
|
+
parseOpenApiEndpoint,
|
|
19
|
+
slugify,
|
|
20
|
+
} from "../utils";
|
|
21
|
+
|
|
22
|
+
type MdxNavItem = string | NavPage | NavGroup | NavOpenApiPage;
|
|
23
|
+
|
|
24
|
+
type ResolvedRouteIndex = {
|
|
25
|
+
canonicalHrefByFilePath: Map<string, string>;
|
|
26
|
+
allHrefsByFilePath: Map<string, string[]>;
|
|
27
|
+
validRoutePaths: Set<string>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type LinkSplit = {
|
|
31
|
+
base: string;
|
|
32
|
+
suffix: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type VFileLike = {
|
|
36
|
+
path?: string;
|
|
37
|
+
message?: (reason: string, place?: unknown) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type MdxJsxAttributeNode = {
|
|
41
|
+
type?: string;
|
|
42
|
+
name?: string;
|
|
43
|
+
value?: unknown;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type MdxJsxElementNode = {
|
|
47
|
+
attributes?: unknown[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const DOCS_ROOT = path.resolve(process.cwd(), "src/content/docs");
|
|
51
|
+
const EXTERNAL_PROTOCOL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
|
|
52
|
+
const HTTP_METHODS = [
|
|
53
|
+
"get",
|
|
54
|
+
"post",
|
|
55
|
+
"put",
|
|
56
|
+
"delete",
|
|
57
|
+
"patch",
|
|
58
|
+
"head",
|
|
59
|
+
"options",
|
|
60
|
+
"trace",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
let cachedRouteIndex: ResolvedRouteIndex | null = null;
|
|
64
|
+
let cachedConfig: DocsConfig | null = null;
|
|
65
|
+
|
|
66
|
+
function normalizeDocsFilePath(value: string): string {
|
|
67
|
+
let normalized = value.replace(/\\/g, "/").trim();
|
|
68
|
+
if (!normalized) return "";
|
|
69
|
+
|
|
70
|
+
normalized = normalized.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
71
|
+
normalized = normalized.replace(/^src\/content\/docs\//, "");
|
|
72
|
+
normalized = normalized.replace(/\.(md|mdx)$/i, "");
|
|
73
|
+
|
|
74
|
+
const posixNormalized = path.posix
|
|
75
|
+
.normalize(`/${normalized}`)
|
|
76
|
+
.replace(/^\/+/, "");
|
|
77
|
+
return posixNormalized === "." ? "" : posixNormalized;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeRoutePath(value: string): string {
|
|
81
|
+
const trimmed = value.trim();
|
|
82
|
+
if (!trimmed || trimmed === "/") {
|
|
83
|
+
return "/";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
87
|
+
const normalized = path.posix.normalize(withLeadingSlash).replace(/\/+$/, "");
|
|
88
|
+
return normalized || "/";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function splitHref(url: string): LinkSplit {
|
|
92
|
+
const match = url.match(/^([^?#]*)(.*)$/);
|
|
93
|
+
return {
|
|
94
|
+
base: match?.[1] ?? url,
|
|
95
|
+
suffix: match?.[2] ?? "",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isLikelyExternalOrNonPageHref(baseHref: string): boolean {
|
|
100
|
+
if (!baseHref) return true;
|
|
101
|
+
if (baseHref.startsWith("#")) return true;
|
|
102
|
+
if (baseHref.startsWith("?")) return true;
|
|
103
|
+
if (baseHref.startsWith("//")) return true;
|
|
104
|
+
return EXTERNAL_PROTOCOL_REGEX.test(baseHref);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeForFileResolution(baseHref: string): string {
|
|
108
|
+
let normalized = baseHref.replace(/\\/g, "/").trim();
|
|
109
|
+
if (!normalized) return "";
|
|
110
|
+
|
|
111
|
+
const docsPathMarker = "/src/content/docs/";
|
|
112
|
+
const markerIndex = normalized.indexOf(docsPathMarker);
|
|
113
|
+
if (markerIndex >= 0) {
|
|
114
|
+
normalized = `/${normalized.slice(markerIndex + docsPathMarker.length)}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
normalized = normalized.replace(/^\/?src\/content\/docs\//, "/");
|
|
118
|
+
normalized = normalized.replace(/\.(md|mdx)$/i, "");
|
|
119
|
+
normalized = normalized.replace(/\/+$/, "");
|
|
120
|
+
normalized = normalized.replace(/\/{2,}/g, "/");
|
|
121
|
+
|
|
122
|
+
return normalized;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isExplicitRelativeTarget(value: string): boolean {
|
|
126
|
+
return value.startsWith("./") || value.startsWith("../");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getCurrentDocFilePath(filePath: string | undefined): string | null {
|
|
130
|
+
if (!filePath) return null;
|
|
131
|
+
|
|
132
|
+
const absoluteFilePath = path.isAbsolute(filePath)
|
|
133
|
+
? filePath
|
|
134
|
+
: path.resolve(process.cwd(), filePath);
|
|
135
|
+
const relativePath = path.relative(DOCS_ROOT, absoluteFilePath);
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
!relativePath ||
|
|
139
|
+
relativePath.startsWith("..") ||
|
|
140
|
+
path.isAbsolute(relativePath)
|
|
141
|
+
) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return normalizeDocsFilePath(relativePath);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildFilePathCandidates(args: {
|
|
149
|
+
baseHref: string;
|
|
150
|
+
currentDocFilePath: string | null;
|
|
151
|
+
}): string[] {
|
|
152
|
+
const rawBase = normalizeForFileResolution(args.baseHref);
|
|
153
|
+
if (!rawBase) return [];
|
|
154
|
+
|
|
155
|
+
const candidates = new Set<string>();
|
|
156
|
+
|
|
157
|
+
if (rawBase.startsWith("/")) {
|
|
158
|
+
const absoluteCandidate = normalizeDocsFilePath(rawBase);
|
|
159
|
+
if (absoluteCandidate) {
|
|
160
|
+
candidates.add(absoluteCandidate);
|
|
161
|
+
}
|
|
162
|
+
return Array.from(candidates);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const currentDocDir = args.currentDocFilePath
|
|
166
|
+
? path.posix.dirname(args.currentDocFilePath)
|
|
167
|
+
: "";
|
|
168
|
+
|
|
169
|
+
const relativeCandidate = normalizeDocsFilePath(
|
|
170
|
+
path.posix.resolve("/", currentDocDir, rawBase).slice(1),
|
|
171
|
+
);
|
|
172
|
+
if (relativeCandidate) {
|
|
173
|
+
candidates.add(relativeCandidate);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!isExplicitRelativeTarget(rawBase)) {
|
|
177
|
+
const rootCandidate = normalizeDocsFilePath(rawBase);
|
|
178
|
+
if (rootCandidate) {
|
|
179
|
+
candidates.add(rootCandidate);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return Array.from(candidates);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function shouldEnforceInternalRouteValidation(baseHref: string): boolean {
|
|
187
|
+
const normalized = normalizeForFileResolution(baseHref);
|
|
188
|
+
if (!normalized) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const extension = path.posix.extname(normalized).toLowerCase();
|
|
193
|
+
if (!extension) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
extension === ".md" ||
|
|
199
|
+
extension === ".mdx" ||
|
|
200
|
+
extension === ".html" ||
|
|
201
|
+
extension === ".htm"
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function addValidRoutePath(index: ResolvedRouteIndex, href: string): string {
|
|
206
|
+
const normalized = normalizeRoutePath(href);
|
|
207
|
+
index.validRoutePaths.add(normalized);
|
|
208
|
+
return normalized;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function addMdxRoute(args: {
|
|
212
|
+
index: ResolvedRouteIndex;
|
|
213
|
+
filePath: string;
|
|
214
|
+
groupSlug: string;
|
|
215
|
+
homePath: string | null;
|
|
216
|
+
}): void {
|
|
217
|
+
const normalizedFilePath = normalizeDocsFilePath(args.filePath);
|
|
218
|
+
if (!normalizedFilePath) return;
|
|
219
|
+
|
|
220
|
+
const navHref = addValidRoutePath(
|
|
221
|
+
args.index,
|
|
222
|
+
buildMdxPageHref({
|
|
223
|
+
filePath: normalizedFilePath,
|
|
224
|
+
groupSlug: args.groupSlug,
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
if (!args.index.canonicalHrefByFilePath.has(normalizedFilePath)) {
|
|
229
|
+
args.index.canonicalHrefByFilePath.set(normalizedFilePath, navHref);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const aliases = args.index.allHrefsByFilePath.get(normalizedFilePath) ?? [];
|
|
233
|
+
if (!aliases.includes(navHref)) {
|
|
234
|
+
aliases.push(navHref);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (args.homePath && normalizedFilePath === args.homePath) {
|
|
238
|
+
addValidRoutePath(args.index, "/");
|
|
239
|
+
if (!aliases.includes("/")) {
|
|
240
|
+
aliases.unshift("/");
|
|
241
|
+
}
|
|
242
|
+
args.index.canonicalHrefByFilePath.set(normalizedFilePath, "/");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
args.index.allHrefsByFilePath.set(normalizedFilePath, aliases);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function addOpenApiEndpointRoute(args: {
|
|
249
|
+
index: ResolvedRouteIndex;
|
|
250
|
+
parentSlug: string;
|
|
251
|
+
method: string;
|
|
252
|
+
endpointPath: string;
|
|
253
|
+
}): void {
|
|
254
|
+
const endpointSlug = buildOpenApiEndpointSlug(args.endpointPath, args.method);
|
|
255
|
+
const fullSlug = args.parentSlug
|
|
256
|
+
? `${args.parentSlug}/${endpointSlug}`
|
|
257
|
+
: endpointSlug;
|
|
258
|
+
addValidRoutePath(args.index, fullSlug);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function shouldIncludeEndpoint(
|
|
262
|
+
method: string,
|
|
263
|
+
pathStr: string,
|
|
264
|
+
include?: string[],
|
|
265
|
+
exclude?: string[],
|
|
266
|
+
): boolean {
|
|
267
|
+
const normalizedMethod = method.toUpperCase();
|
|
268
|
+
const normalizedPath = pathStr.toLowerCase();
|
|
269
|
+
const endpointKey = `${normalizedMethod} ${normalizedPath}`;
|
|
270
|
+
|
|
271
|
+
if (include) {
|
|
272
|
+
return include.some((entry) => {
|
|
273
|
+
const parsed = parseOpenApiEndpoint(entry);
|
|
274
|
+
if (!parsed) return false;
|
|
275
|
+
return `${parsed.method} ${parsed.path}` === endpointKey;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (exclude) {
|
|
280
|
+
return !exclude.some((entry) => {
|
|
281
|
+
const parsed = parseOpenApiEndpoint(entry);
|
|
282
|
+
if (!parsed) return false;
|
|
283
|
+
return `${parsed.method} ${parsed.path}` === endpointKey;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function processOpenApiFile(args: {
|
|
291
|
+
index: ResolvedRouteIndex;
|
|
292
|
+
parentSlug: string;
|
|
293
|
+
openApiPathOrConfig: string | NavOpenApi;
|
|
294
|
+
}): Promise<void> {
|
|
295
|
+
let openApiSource: string;
|
|
296
|
+
let include: string[] | undefined;
|
|
297
|
+
let exclude: string[] | undefined;
|
|
298
|
+
|
|
299
|
+
if (typeof args.openApiPathOrConfig === "string") {
|
|
300
|
+
openApiSource = args.openApiPathOrConfig;
|
|
301
|
+
} else {
|
|
302
|
+
openApiSource = args.openApiPathOrConfig.source;
|
|
303
|
+
include = args.openApiPathOrConfig.include;
|
|
304
|
+
exclude = args.openApiPathOrConfig.exclude;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let openApiDoc: any;
|
|
308
|
+
try {
|
|
309
|
+
openApiDoc = await loadOpenApiSpec(openApiSource);
|
|
310
|
+
} catch {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const paths = openApiDoc?.paths ?? {};
|
|
315
|
+
for (const [pathStr, pathItem] of Object.entries(paths)) {
|
|
316
|
+
if (!pathItem || typeof pathItem !== "object") continue;
|
|
317
|
+
|
|
318
|
+
for (const method of HTTP_METHODS) {
|
|
319
|
+
const operation = (pathItem as any)[method];
|
|
320
|
+
if (!operation) continue;
|
|
321
|
+
|
|
322
|
+
if (!shouldIncludeEndpoint(method, pathStr, include, exclude)) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
addOpenApiEndpointRoute({
|
|
327
|
+
index: args.index,
|
|
328
|
+
parentSlug: args.parentSlug,
|
|
329
|
+
method,
|
|
330
|
+
endpointPath: pathStr,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function processOpenApiPage(args: {
|
|
337
|
+
index: ResolvedRouteIndex;
|
|
338
|
+
parentSlug: string;
|
|
339
|
+
item: NavOpenApiPage;
|
|
340
|
+
}): void {
|
|
341
|
+
const parsed = parseOpenApiEndpoint(args.item.openapi.endpoint);
|
|
342
|
+
if (!parsed) return;
|
|
343
|
+
|
|
344
|
+
addOpenApiEndpointRoute({
|
|
345
|
+
index: args.index,
|
|
346
|
+
parentSlug: args.parentSlug,
|
|
347
|
+
method: parsed.method,
|
|
348
|
+
endpointPath: parsed.path,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function processPages(args: {
|
|
353
|
+
index: ResolvedRouteIndex;
|
|
354
|
+
parentSlug: string;
|
|
355
|
+
homePath: string | null;
|
|
356
|
+
items: MdxNavItem[];
|
|
357
|
+
}): Promise<void> {
|
|
358
|
+
for (const item of args.items) {
|
|
359
|
+
if (typeof item === "string") {
|
|
360
|
+
addMdxRoute({
|
|
361
|
+
index: args.index,
|
|
362
|
+
filePath: item,
|
|
363
|
+
groupSlug: args.parentSlug,
|
|
364
|
+
homePath: args.homePath,
|
|
365
|
+
});
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if ("page" in item) {
|
|
370
|
+
const pageItem = item as NavPage;
|
|
371
|
+
addMdxRoute({
|
|
372
|
+
index: args.index,
|
|
373
|
+
filePath: pageItem.page,
|
|
374
|
+
groupSlug: args.parentSlug,
|
|
375
|
+
homePath: args.homePath,
|
|
376
|
+
});
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if ("group" in item) {
|
|
381
|
+
const group = item as NavGroup;
|
|
382
|
+
const groupSlug = slugify(group.group);
|
|
383
|
+
const nextPrefix = args.parentSlug
|
|
384
|
+
? `${args.parentSlug}/${groupSlug}`
|
|
385
|
+
: groupSlug;
|
|
386
|
+
|
|
387
|
+
await processPages({
|
|
388
|
+
index: args.index,
|
|
389
|
+
parentSlug: nextPrefix,
|
|
390
|
+
homePath: args.homePath,
|
|
391
|
+
items: group.pages as MdxNavItem[],
|
|
392
|
+
});
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if ("openapi" in item) {
|
|
397
|
+
processOpenApiPage({
|
|
398
|
+
index: args.index,
|
|
399
|
+
parentSlug: args.parentSlug,
|
|
400
|
+
item: item as NavOpenApiPage,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function processMenuItems(args: {
|
|
407
|
+
index: ResolvedRouteIndex;
|
|
408
|
+
homePath: string | null;
|
|
409
|
+
items: NavMenuItem[];
|
|
410
|
+
}): Promise<void> {
|
|
411
|
+
for (const menuItem of args.items) {
|
|
412
|
+
const menuSlug = slugify(menuItem.label);
|
|
413
|
+
const parentSlug = menuSlug;
|
|
414
|
+
|
|
415
|
+
if (Array.isArray(menuItem.submenu.pages)) {
|
|
416
|
+
await processPages({
|
|
417
|
+
index: args.index,
|
|
418
|
+
parentSlug,
|
|
419
|
+
homePath: args.homePath,
|
|
420
|
+
items: menuItem.submenu.pages as MdxNavItem[],
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (menuItem.submenu.openapi) {
|
|
425
|
+
await processOpenApiFile({
|
|
426
|
+
index: args.index,
|
|
427
|
+
parentSlug,
|
|
428
|
+
openApiPathOrConfig: menuItem.submenu.openapi,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function buildRouteIndex(config: DocsConfig): Promise<ResolvedRouteIndex> {
|
|
435
|
+
const index: ResolvedRouteIndex = {
|
|
436
|
+
canonicalHrefByFilePath: new Map<string, string>(),
|
|
437
|
+
allHrefsByFilePath: new Map<string, string[]>(),
|
|
438
|
+
validRoutePaths: new Set<string>(),
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const homePath =
|
|
442
|
+
typeof config.home === "string" ? normalizeDocsFilePath(config.home) : null;
|
|
443
|
+
|
|
444
|
+
if (Array.isArray(config.navigation.pages)) {
|
|
445
|
+
await processPages({
|
|
446
|
+
index,
|
|
447
|
+
parentSlug: "",
|
|
448
|
+
homePath,
|
|
449
|
+
items: config.navigation.pages as MdxNavItem[],
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (config.navigation.menu?.items) {
|
|
454
|
+
await processMenuItems({
|
|
455
|
+
index,
|
|
456
|
+
homePath,
|
|
457
|
+
items: config.navigation.menu.items,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const rootOpenApi = (config.navigation as any).openapi;
|
|
462
|
+
if (typeof rootOpenApi === "string" || (rootOpenApi && typeof rootOpenApi === "object")) {
|
|
463
|
+
await processOpenApiFile({
|
|
464
|
+
index,
|
|
465
|
+
parentSlug: "",
|
|
466
|
+
openApiPathOrConfig: rootOpenApi,
|
|
467
|
+
});
|
|
468
|
+
} else if (Array.isArray(rootOpenApi)) {
|
|
469
|
+
for (const openApiItem of rootOpenApi) {
|
|
470
|
+
if (typeof openApiItem !== "string" && (!openApiItem || typeof openApiItem !== "object")) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
await processOpenApiFile({
|
|
475
|
+
index,
|
|
476
|
+
parentSlug: "",
|
|
477
|
+
openApiPathOrConfig: openApiItem as string | NavOpenApi,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (homePath) {
|
|
483
|
+
addValidRoutePath(index, "/");
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return index;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function getRouteIndex(): Promise<ResolvedRouteIndex> {
|
|
490
|
+
const config = await getConfig();
|
|
491
|
+
if (cachedRouteIndex && cachedConfig === config) {
|
|
492
|
+
return cachedRouteIndex;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
cachedConfig = config;
|
|
496
|
+
cachedRouteIndex = await buildRouteIndex(config);
|
|
497
|
+
return cachedRouteIndex;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function buildInvalidInternalLinkError(args: {
|
|
501
|
+
baseHref: string;
|
|
502
|
+
currentDocFilePath: string | null;
|
|
503
|
+
filePathCandidates: string[];
|
|
504
|
+
}): Error {
|
|
505
|
+
const sourceFile = args.currentDocFilePath
|
|
506
|
+
? `${args.currentDocFilePath}.mdx`
|
|
507
|
+
: "the current MDX file";
|
|
508
|
+
|
|
509
|
+
const candidateHint =
|
|
510
|
+
args.filePathCandidates.length > 0
|
|
511
|
+
? ` Tried file path candidates: ${args.filePathCandidates
|
|
512
|
+
.map((candidate) => `"${candidate}"`)
|
|
513
|
+
.join(", ")}.`
|
|
514
|
+
: "";
|
|
515
|
+
|
|
516
|
+
return new Error(
|
|
517
|
+
`[USER_ERROR]: Invalid internal link "${args.baseHref}" in ${sourceFile}. ` +
|
|
518
|
+
`Link must match an existing docs URL or resolve to a docs page file in navigation.${candidateHint}`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function resolveLinkToCanonicalHref(args: {
|
|
523
|
+
rawHref: string;
|
|
524
|
+
currentDocFilePath: string | null;
|
|
525
|
+
routeIndex: ResolvedRouteIndex;
|
|
526
|
+
warn: (message: string) => void;
|
|
527
|
+
}): string | null {
|
|
528
|
+
const parts = splitHref(args.rawHref);
|
|
529
|
+
const baseHref = parts.base.trim();
|
|
530
|
+
if (isLikelyExternalOrNonPageHref(baseHref)) {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const normalizedBaseRoute = normalizeRoutePath(baseHref);
|
|
535
|
+
if (args.routeIndex.validRoutePaths.has(normalizedBaseRoute)) {
|
|
536
|
+
if (baseHref.startsWith("/")) {
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return `${normalizedBaseRoute}${parts.suffix}`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const filePathCandidates = buildFilePathCandidates({
|
|
544
|
+
baseHref,
|
|
545
|
+
currentDocFilePath: args.currentDocFilePath,
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
for (const filePath of filePathCandidates) {
|
|
549
|
+
const canonicalHref = args.routeIndex.canonicalHrefByFilePath.get(filePath);
|
|
550
|
+
if (!canonicalHref) continue;
|
|
551
|
+
|
|
552
|
+
const aliases = args.routeIndex.allHrefsByFilePath.get(filePath) ?? [];
|
|
553
|
+
if (aliases.length > 1) {
|
|
554
|
+
args.warn(
|
|
555
|
+
`[INTERNAL_LINK_WARNING] "${baseHref}" matched "${filePath}", which has multiple URLs (${aliases.join(", ")}). Rewriting to canonical URL "${canonicalHref}".`,
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return `${canonicalHref}${parts.suffix}`;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (shouldEnforceInternalRouteValidation(baseHref)) {
|
|
563
|
+
throw buildInvalidInternalLinkError({
|
|
564
|
+
baseHref,
|
|
565
|
+
currentDocFilePath: args.currentDocFilePath,
|
|
566
|
+
filePathCandidates,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export const remarkResolveInternalLinks: Plugin<[], Root> = () => {
|
|
574
|
+
return async (tree, file) => {
|
|
575
|
+
const routeIndex = await getRouteIndex();
|
|
576
|
+
const vfile = file as VFileLike;
|
|
577
|
+
const currentDocFilePath = getCurrentDocFilePath(vfile.path);
|
|
578
|
+
const emittedWarnings = new Set<string>();
|
|
579
|
+
|
|
580
|
+
const warnOnce = (message: string, place: unknown): void => {
|
|
581
|
+
if (emittedWarnings.has(message)) return;
|
|
582
|
+
emittedWarnings.add(message);
|
|
583
|
+
if (typeof vfile.message === "function") {
|
|
584
|
+
vfile.message(message, place);
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
visitParents(tree, "link", (node) => {
|
|
589
|
+
const linkNode = node as Link;
|
|
590
|
+
if (typeof linkNode.url !== "string") return;
|
|
591
|
+
|
|
592
|
+
const rewrittenHref = resolveLinkToCanonicalHref({
|
|
593
|
+
rawHref: linkNode.url,
|
|
594
|
+
currentDocFilePath,
|
|
595
|
+
routeIndex,
|
|
596
|
+
warn: (message) => warnOnce(message, node),
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
if (rewrittenHref && rewrittenHref !== linkNode.url) {
|
|
600
|
+
linkNode.url = rewrittenHref;
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const rewriteMdxJsxHrefAttributes = (node: unknown): void => {
|
|
605
|
+
const element = node as MdxJsxElementNode;
|
|
606
|
+
if (!Array.isArray(element.attributes)) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
for (const attribute of element.attributes) {
|
|
611
|
+
const hrefAttribute = attribute as MdxJsxAttributeNode;
|
|
612
|
+
if (hrefAttribute.type !== "mdxJsxAttribute") continue;
|
|
613
|
+
if (hrefAttribute.name !== "href") continue;
|
|
614
|
+
if (typeof hrefAttribute.value !== "string") continue;
|
|
615
|
+
|
|
616
|
+
const rewrittenHref = resolveLinkToCanonicalHref({
|
|
617
|
+
rawHref: hrefAttribute.value,
|
|
618
|
+
currentDocFilePath,
|
|
619
|
+
routeIndex,
|
|
620
|
+
warn: (message) => warnOnce(message, node),
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
if (rewrittenHref && rewrittenHref !== hrefAttribute.value) {
|
|
624
|
+
hrefAttribute.value = rewrittenHref;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
visitParents(tree, "mdxJsxFlowElement", (node) => {
|
|
630
|
+
rewriteMdxJsxHrefAttributes(node);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
visitParents(tree, "mdxJsxTextElement", (node) => {
|
|
634
|
+
rewriteMdxJsxHrefAttributes(node);
|
|
635
|
+
});
|
|
636
|
+
};
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
export default remarkResolveInternalLinks;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Icon } from "astro-icon/components";
|
|
3
|
+
import Layout from "../layouts/Layout.astro";
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<Layout
|
|
7
|
+
pageTitle="Page not found"
|
|
8
|
+
pageDescription="The page you requested could not be found."
|
|
9
|
+
>
|
|
10
|
+
<section
|
|
11
|
+
class="mx-auto mt-8 h-full my-auto flex max-w-xl flex-col items-center gap-5 rounded-2xl bg-background px-6 py-10 text-center"
|
|
12
|
+
>
|
|
13
|
+
<p
|
|
14
|
+
class="font-mono text-sm font-medium tracking-[0.22em] text-neutral-500 uppercase dark:text-neutral-400 border px-1 py-px pr-0.5 rounded"
|
|
15
|
+
>
|
|
16
|
+
404
|
|
17
|
+
</p>
|
|
18
|
+
<h1
|
|
19
|
+
class="text-3xl font-semibold tracking-tight text-neutral-900 dark:text-neutral-50"
|
|
20
|
+
>
|
|
21
|
+
Page not found
|
|
22
|
+
</h1>
|
|
23
|
+
<p class="max-w-md text-sm text-neutral-600 dark:text-neutral-300">
|
|
24
|
+
The page may have moved, or the URL may be incorrect.
|
|
25
|
+
</p>
|
|
26
|
+
<div class="flex flex-wrap items-center justify-center gap-3">
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
class="inline-flex items-center justify-center gap-1.5 rounded-lg [corner-shape:superellipse(1.2)] border shadow-xs px-4 py-2 text-sm font-medium text-neutral-700/85 hover:text-neutral-700 cursor-pointer"
|
|
30
|
+
onclick="history.back()"
|
|
31
|
+
>
|
|
32
|
+
<Icon name="lucide:arrow-big-left-dash" class="size-4" />
|
|
33
|
+
Go back
|
|
34
|
+
</button>
|
|
35
|
+
<a
|
|
36
|
+
href="/"
|
|
37
|
+
class="inline-flex items-center justify-center gap-2 rounded-lg [corner-shape:superellipse(1.2)] border border-border px-4 py-2 text-sm font-[350] dark:font-[450] text-white bg-linear-to-b from-neutral-900/85 to-neutral-900 dark:from-neutral-100 dark:to-neutral-200 shadow-sm"
|
|
38
|
+
>
|
|
39
|
+
<Icon name="lucide:house" class="size-4" />
|
|
40
|
+
Take me home
|
|
41
|
+
</a>
|
|
42
|
+
</div>
|
|
43
|
+
</section>
|
|
44
|
+
</Layout>
|