radiant-docs 0.1.56 → 0.1.57

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.
Files changed (33) hide show
  1. package/dist/index.js +3 -76
  2. package/package.json +2 -4
  3. package/template/astro.config.mjs +16 -26
  4. package/template/package-lock.json +18 -0
  5. package/template/package.json +1 -1
  6. package/template/src/components/Footer.astro +13 -4
  7. package/template/src/components/Header.astro +26 -6
  8. package/template/src/components/SidebarLink.astro +3 -2
  9. package/template/src/components/SidebarSubgroup.astro +14 -13
  10. package/template/src/components/sidebar/SidebarEndpointLink.astro +13 -3
  11. package/template/src/components/sidebar/SidebarOpenApi.astro +2 -0
  12. package/template/src/components/sidebar/SidebarOpenApiPageLink.astro +1 -0
  13. package/template/src/components/user/Accordion.astro +0 -13
  14. package/template/src/components/user/Callout.astro +0 -29
  15. package/template/src/components/user/Card.astro +31 -204
  16. package/template/src/components/user/CardGradient.astro +8 -1
  17. package/template/src/components/user/Column.astro +0 -17
  18. package/template/src/components/user/Columns.astro +4 -153
  19. package/template/src/components/user/Image.astro +0 -28
  20. package/template/src/components/user/Step.astro +0 -10
  21. package/template/src/components/user/Tab.astro +0 -12
  22. package/template/src/components/user/Tabs.astro +2 -9
  23. package/template/src/content.config.ts +1 -1
  24. package/template/src/lib/code/code-block.ts +1 -1
  25. package/template/src/lib/mdx/remark-code-block-component.ts +1 -20
  26. package/template/src/lib/mdx/remark-resolve-internal-links.ts +150 -204
  27. package/template/src/lib/routes.ts +150 -29
  28. package/template/src/lib/utils.ts +127 -12
  29. package/template/src/lib/validation.ts +5 -2826
  30. package/template/src/pages/[...slug].astro +16 -0
  31. package/template/src/lib/code/shiki-theme-config.ts +0 -16
  32. package/template/src/lib/component-error.ts +0 -202
  33. package/template/src/lib/frontmatter-schema.ts +0 -10
@@ -9,11 +9,16 @@ import {
9
9
  loadOpenApiSpec,
10
10
  } from "./validation";
11
11
  import {
12
+ buildMdxPageFallbackSlug,
13
+ buildMdxPageSlug,
12
14
  buildOpenApiEndpointSlug,
15
+ buildOpenApiEndpointFallbackSlug,
16
+ createRouteSlugRegistry,
13
17
  deriveTitleFromEntryId,
14
18
  parseOpenApiEndpoint,
15
19
  slugify,
16
20
  } from "./utils";
21
+ import { prependBasePath, withBasePath } from "./base-path";
17
22
  import { getCollection } from "astro:content";
18
23
 
19
24
  type MdxNavPageItem = string | NavPage;
@@ -21,6 +26,9 @@ type MdxNavPageItem = string | NavPage;
21
26
  // Base route interface
22
27
  export interface BaseRoute {
23
28
  slug: string;
29
+ preferredSlug: string;
30
+ fallbackSlug?: string;
31
+ routeIdentity: string;
24
32
  title: string;
25
33
  hidden?: boolean;
26
34
  }
@@ -56,7 +64,9 @@ export function resolveMdxPageTitle(args: {
56
64
  const frontmatterTitle = normalizeTitle(args.entry?.data?.title);
57
65
  const docsJsonTitle = normalizeTitle(args.docsTitle);
58
66
 
59
- return frontmatterTitle || docsJsonTitle || deriveTitleFromEntryId(args.filePath);
67
+ return (
68
+ frontmatterTitle || docsJsonTitle || deriveTitleFromEntryId(args.filePath)
69
+ );
60
70
  }
61
71
 
62
72
  function processPageItem(
@@ -65,9 +75,10 @@ function processPageItem(
65
75
  docs: any[],
66
76
  ): MdxRoute {
67
77
  const filePath = typeof item === "string" ? item : item.page;
68
- const filename = filePath.split("/").pop() || filePath;
69
- const pageSlug = slugify(filename);
70
- const fullSlug = parentSlug ? `${parentSlug}/${pageSlug}` : pageSlug;
78
+ const preferredSlug = buildMdxPageSlug({
79
+ filePath,
80
+ groupSlug: parentSlug,
81
+ });
71
82
 
72
83
  // Find the entry matching this filePath
73
84
  const entry = docs.find((doc: any) => {
@@ -91,16 +102,19 @@ function processPageItem(
91
102
 
92
103
  return {
93
104
  type: "mdx",
94
- slug: fullSlug,
105
+ slug: preferredSlug,
106
+ preferredSlug,
107
+ fallbackSlug: buildMdxPageFallbackSlug({
108
+ filePath,
109
+ groupSlug: parentSlug,
110
+ }),
111
+ routeIdentity: `mdx:${filePath}`,
95
112
  filePath: filePath,
96
113
  title: title,
97
114
  };
98
115
  }
99
116
 
100
- function processHiddenPageRoute(
101
- route: HiddenPageRoute,
102
- docs: any[],
103
- ): MdxRoute {
117
+ function processHiddenPageRoute(route: HiddenPageRoute, docs: any[]): MdxRoute {
104
118
  const entry = docs.find((doc: any) => {
105
119
  const docPath = doc.id.replace(/\.mdx$/, "");
106
120
  return docPath === route.filePath;
@@ -117,6 +131,12 @@ function processHiddenPageRoute(
117
131
  return {
118
132
  type: "mdx",
119
133
  slug,
134
+ preferredSlug: slug,
135
+ fallbackSlug: buildMdxPageFallbackSlug({
136
+ filePath: route.filePath,
137
+ groupSlug: "",
138
+ }),
139
+ routeIdentity: `mdx:${route.filePath}`,
120
140
  filePath: route.filePath,
121
141
  title: resolveMdxPageTitle({
122
142
  entry,
@@ -195,6 +215,14 @@ async function processOpenApiPageItem(
195
215
  return {
196
216
  type: "openapi",
197
217
  slug: fullSlug,
218
+ preferredSlug: fullSlug,
219
+ fallbackSlug: buildOpenApiEndpointFallbackSlug({
220
+ source: item.openapi.source,
221
+ path: matchedOperation.path,
222
+ method: matchedOperation.method,
223
+ groupSlug: parentSlug,
224
+ }),
225
+ routeIdentity: `openapi:${item.openapi.source}:${matchedOperation.method.toUpperCase()} ${matchedOperation.path.toLowerCase()}`,
198
226
  filePath: item.openapi.source,
199
227
  title,
200
228
  openApiPath: matchedOperation.path,
@@ -328,6 +356,14 @@ async function processOpenApiFile(
328
356
  routes.push({
329
357
  type: "openapi",
330
358
  slug: fullSlug,
359
+ preferredSlug: fullSlug,
360
+ fallbackSlug: buildOpenApiEndpointFallbackSlug({
361
+ source: openApiPath,
362
+ path: pathStr,
363
+ method,
364
+ groupSlug: parentSlug,
365
+ }),
366
+ routeIdentity: `openapi:${openApiPath}:${method.toUpperCase()} ${pathStr.toLowerCase()}`,
331
367
  filePath: openApiPath,
332
368
  title: title,
333
369
  openApiPath: pathStr,
@@ -379,29 +415,28 @@ async function processMenuItem(
379
415
  return routes;
380
416
  }
381
417
 
382
- function assertUniqueRouteSlugs(routes: Route[]): void {
383
- const seenBySlug = new Map<string, Route>();
418
+ function resolveRouteSlugs(routes: Route[]): Route[] {
419
+ const registry = createRouteSlugRegistry();
420
+ const resolvedRoutes: Route[] = [];
384
421
 
385
422
  for (const route of routes) {
386
- const existing = seenBySlug.get(route.slug);
387
- if (!existing) {
388
- seenBySlug.set(route.slug, route);
423
+ const registered = registry.register({
424
+ preferredSlug: route.preferredSlug,
425
+ fallbackSlug: route.fallbackSlug,
426
+ identity: route.routeIdentity,
427
+ });
428
+
429
+ if (registered.duplicate) {
389
430
  continue;
390
431
  }
391
432
 
392
- const existingLabel =
393
- existing.type === "mdx"
394
- ? `mdx:${existing.filePath}`
395
- : `openapi:${existing.filePath}:${existing.openApiMethod.toUpperCase()} ${existing.openApiPath}`;
396
- const candidateLabel =
397
- route.type === "mdx"
398
- ? `mdx:${route.filePath}`
399
- : `openapi:${route.filePath}:${route.openApiMethod.toUpperCase()} ${route.openApiPath}`;
400
-
401
- throw new Error(
402
- `[USER_ERROR]: Invalid docs.json: Duplicate route slug "${route.slug}" generated by "${existingLabel}" and "${candidateLabel}". Remove one of the duplicate references or change navigation structure so each route resolves to a unique URL.`,
403
- );
433
+ resolvedRoutes.push({
434
+ ...route,
435
+ slug: registered.slug,
436
+ });
404
437
  }
438
+
439
+ return resolvedRoutes;
405
440
  }
406
441
 
407
442
  export async function getAllRoutes(): Promise<Route[]> {
@@ -422,7 +457,9 @@ export async function getAllRoutes(): Promise<Route[]> {
422
457
  if (typeof item === "string" || "page" in item) {
423
458
  allRoutes.push(processPageItem(item, "", docs));
424
459
  } else if ("group" in item) {
425
- allRoutes = allRoutes.concat(await processGroup(item as NavGroup, "", docs));
460
+ allRoutes = allRoutes.concat(
461
+ await processGroup(item as NavGroup, "", docs),
462
+ );
426
463
  } else if ("openapi" in item) {
427
464
  allRoutes.push(await processOpenApiPageItem(item, ""));
428
465
  }
@@ -434,6 +471,9 @@ export async function getAllRoutes(): Promise<Route[]> {
434
471
  const menuItemRoutes = await processMenuItem(menuItem, "", docs);
435
472
  allRoutes = allRoutes.concat(menuItemRoutes);
436
473
  }
474
+ } else if (navigation.openapi) {
475
+ const openApiRoutes = await processOpenApiFile(navigation.openapi, "");
476
+ allRoutes = allRoutes.concat(openApiRoutes);
437
477
  }
438
478
 
439
479
  for (const hiddenPageRoute of config.hiddenPageRoutes ?? []) {
@@ -453,6 +493,87 @@ export async function getAllRoutes(): Promise<Route[]> {
453
493
  allRoutes.push(hiddenRoute);
454
494
  }
455
495
 
456
- assertUniqueRouteSlugs(allRoutes);
457
- return allRoutes;
496
+ return resolveRouteSlugs(allRoutes);
497
+ }
498
+
499
+ export async function getMdxRouteHref(args: {
500
+ filePath: string;
501
+ groupSlug?: string;
502
+ homePath?: string;
503
+ preferredSlug?: string;
504
+ }): Promise<string> {
505
+ if (!args.preferredSlug && args.homePath && args.filePath === args.homePath) {
506
+ return prependBasePath("/");
507
+ }
508
+
509
+ const preferredSlug =
510
+ args.preferredSlug?.replace(/^\/+/, "").replace(/\/+$/, "") ||
511
+ buildMdxPageSlug({
512
+ filePath: args.filePath,
513
+ groupSlug: args.groupSlug,
514
+ });
515
+ const routes = await getAllRoutes();
516
+ const route = routes.find(
517
+ (candidate) =>
518
+ candidate.type === "mdx" &&
519
+ candidate.filePath === args.filePath &&
520
+ candidate.preferredSlug === preferredSlug,
521
+ );
522
+
523
+ return prependBasePath(`/${route?.slug ?? preferredSlug}`);
524
+ }
525
+
526
+ export async function resolveConfiguredHref(
527
+ href: string,
528
+ hiddenPageRoutes: HiddenPageRoute[] | undefined,
529
+ ): Promise<string> {
530
+ const normalizedHref = href.replace(/^\/+/, "").replace(/\/+$/, "");
531
+ const hiddenPageRoute = hiddenPageRoutes?.find(
532
+ (route) => route.href.replace(/^\/+/, "").replace(/\/+$/, "") === normalizedHref,
533
+ );
534
+
535
+ if (!hiddenPageRoute) {
536
+ return withBasePath(href);
537
+ }
538
+
539
+ return getMdxRouteHref({
540
+ filePath: hiddenPageRoute.filePath,
541
+ preferredSlug: hiddenPageRoute.href,
542
+ });
543
+ }
544
+
545
+ export async function getOpenApiEndpointRouteHref(args: {
546
+ source: string;
547
+ path: string;
548
+ method: string;
549
+ groupSlug?: string;
550
+ }): Promise<string> {
551
+ const endpointSlug = buildOpenApiEndpointSlug(args.path, args.method);
552
+ const normalizedGroupSlug = (args.groupSlug || "")
553
+ .replace(/^\/+/, "")
554
+ .replace(/\/+$/, "");
555
+ const preferredSlug = normalizedGroupSlug
556
+ ? `${normalizedGroupSlug}/${endpointSlug}`
557
+ : endpointSlug;
558
+ const config = await getConfig();
559
+ const routes = await getAllRoutes();
560
+ const visibleRoutes = routes.filter((candidate) => !candidate.hidden);
561
+ const route = routes.find(
562
+ (candidate) =>
563
+ candidate.type === "openapi" &&
564
+ candidate.filePath === args.source &&
565
+ candidate.openApiMethod.toLowerCase() === args.method.toLowerCase() &&
566
+ candidate.openApiPath.toLowerCase() === args.path.toLowerCase() &&
567
+ candidate.preferredSlug === preferredSlug,
568
+ );
569
+
570
+ if (
571
+ route &&
572
+ !config.home &&
573
+ visibleRoutes[0] === route
574
+ ) {
575
+ return prependBasePath("/");
576
+ }
577
+
578
+ return prependBasePath(`/${route?.slug ?? preferredSlug}`);
458
579
  }
@@ -61,6 +61,38 @@ export function deriveTitleFromEntryId(filePath: string): string {
61
61
  );
62
62
  }
63
63
 
64
+ function normalizeSlugPath(value: string): string {
65
+ return value.replace(/^\/+/, "").replace(/\/+$/, "");
66
+ }
67
+
68
+ export function buildMdxPageSlug(args: {
69
+ filePath: string;
70
+ groupSlug?: string;
71
+ }): string {
72
+ const filename = path.basename(args.filePath);
73
+ const pageSlug = slugify(filename);
74
+ const normalizedGroupSlug = normalizeSlugPath(args.groupSlug || "");
75
+
76
+ return normalizedGroupSlug ? `${normalizedGroupSlug}/${pageSlug}` : pageSlug;
77
+ }
78
+
79
+ export function buildMdxPageFallbackSlug(args: {
80
+ filePath: string;
81
+ groupSlug?: string;
82
+ }): string {
83
+ const pathSlug = args.filePath
84
+ .split(/[\\/]+/)
85
+ .map((segment) => slugify(segment.replace(/\.(md|mdx)$/i, "")))
86
+ .filter(Boolean)
87
+ .join("-");
88
+ const normalizedGroupSlug = normalizeSlugPath(args.groupSlug || "");
89
+ const fallbackSlug = pathSlug || buildMdxPageSlug(args);
90
+
91
+ return normalizedGroupSlug
92
+ ? `${normalizedGroupSlug}/${fallbackSlug}`
93
+ : fallbackSlug;
94
+ }
95
+
64
96
  export function buildMdxPageHref(args: {
65
97
  filePath: string;
66
98
  groupSlug?: string;
@@ -70,15 +102,7 @@ export function buildMdxPageHref(args: {
70
102
  return prependBasePath("/");
71
103
  }
72
104
 
73
- const filename = path.basename(args.filePath);
74
- const pageSlug = slugify(filename);
75
- const normalizedGroupSlug = (args.groupSlug || "")
76
- .replace(/^\/+/, "")
77
- .replace(/\/+$/, "");
78
-
79
- const href = normalizedGroupSlug
80
- ? `/${normalizedGroupSlug}/${pageSlug}`
81
- : `/${pageSlug}`;
105
+ const href = `/${buildMdxPageSlug(args)}`;
82
106
 
83
107
  return prependBasePath(href);
84
108
  }
@@ -119,15 +143,35 @@ export function buildOpenApiEndpointSlug(
119
143
  return pathSlug ? `${pathSlug}-${methodSlug}` : methodSlug;
120
144
  }
121
145
 
146
+ export function buildOpenApiEndpointFallbackSlug(args: {
147
+ source: string;
148
+ path: string;
149
+ method: string;
150
+ groupSlug?: string;
151
+ }): string {
152
+ const sourceSlug =
153
+ args.source
154
+ .replace(/\.(json|ya?ml)$/i, "")
155
+ .split(/[\\/]+/)
156
+ .map((segment) => slugify(segment))
157
+ .filter(Boolean)
158
+ .join("-") || "openapi";
159
+ const endpointSlug = buildOpenApiEndpointSlug(args.path, args.method);
160
+ const normalizedGroupSlug = normalizeSlugPath(args.groupSlug || "");
161
+ const fallbackSlug = `${sourceSlug}-${endpointSlug}`;
162
+
163
+ return normalizedGroupSlug
164
+ ? `${normalizedGroupSlug}/${fallbackSlug}`
165
+ : fallbackSlug;
166
+ }
167
+
122
168
  export function buildOpenApiEndpointHref(args: {
123
169
  path: string;
124
170
  method: string;
125
171
  groupSlug?: string;
126
172
  }): string {
127
173
  const endpointSlug = buildOpenApiEndpointSlug(args.path, args.method);
128
- const normalizedGroupSlug = (args.groupSlug || "")
129
- .replace(/^\/+/, "")
130
- .replace(/\/+$/, "");
174
+ const normalizedGroupSlug = normalizeSlugPath(args.groupSlug || "");
131
175
 
132
176
  const href = normalizedGroupSlug
133
177
  ? `/${normalizedGroupSlug}/${endpointSlug}`
@@ -135,3 +179,74 @@ export function buildOpenApiEndpointHref(args: {
135
179
 
136
180
  return prependBasePath(href);
137
181
  }
182
+
183
+ export type RouteSlugRegistration = {
184
+ preferredSlug: string;
185
+ fallbackSlug?: string;
186
+ identity: string;
187
+ };
188
+
189
+ export function createRouteSlugRegistry() {
190
+ const slugIdentityBySlug = new Map<string, string>();
191
+ const slugByExactRoute = new Map<string, string>();
192
+
193
+ function normalizeCandidate(value: string | undefined): string {
194
+ const normalized = normalizeSlugPath(value || "");
195
+ return normalized || "page";
196
+ }
197
+
198
+ function nextAvailableSlug(baseSlug: string): string {
199
+ let index = 2;
200
+ let candidate = baseSlug;
201
+
202
+ while (slugIdentityBySlug.has(candidate)) {
203
+ candidate = `${baseSlug}-${index}`;
204
+ index += 1;
205
+ }
206
+
207
+ return candidate;
208
+ }
209
+
210
+ return {
211
+ register({ preferredSlug, fallbackSlug, identity }: RouteSlugRegistration) {
212
+ const preferred = normalizeCandidate(preferredSlug);
213
+ const exactKey = `${preferred}\0${identity}`;
214
+ const existingExactSlug = slugByExactRoute.get(exactKey);
215
+
216
+ if (existingExactSlug) {
217
+ return {
218
+ slug: existingExactSlug,
219
+ duplicate: true,
220
+ };
221
+ }
222
+
223
+ const preferredIdentity = slugIdentityBySlug.get(preferred);
224
+ if (!preferredIdentity) {
225
+ slugIdentityBySlug.set(preferred, identity);
226
+ slugByExactRoute.set(exactKey, preferred);
227
+ return {
228
+ slug: preferred,
229
+ duplicate: false,
230
+ };
231
+ }
232
+
233
+ if (preferredIdentity === identity) {
234
+ slugByExactRoute.set(exactKey, preferred);
235
+ return {
236
+ slug: preferred,
237
+ duplicate: true,
238
+ };
239
+ }
240
+
241
+ const fallbackBase = normalizeCandidate(fallbackSlug || preferred);
242
+ const resolvedSlug = nextAvailableSlug(fallbackBase);
243
+ slugIdentityBySlug.set(resolvedSlug, identity);
244
+ slugByExactRoute.set(exactKey, resolvedSlug);
245
+
246
+ return {
247
+ slug: resolvedSlug,
248
+ duplicate: false,
249
+ };
250
+ },
251
+ };
252
+ }