radiant-docs 0.1.39 → 0.1.40

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 (34) hide show
  1. package/package.json +1 -1
  2. package/template/astro.config.mjs +38 -7
  3. package/template/package-lock.json +19 -7
  4. package/template/package.json +1 -1
  5. package/template/scripts/generate-robots-txt.mjs +29 -1
  6. package/template/scripts/stamp-image-versions.mjs +59 -33
  7. package/template/src/components/Footer.astro +2 -1
  8. package/template/src/components/Header.astro +8 -6
  9. package/template/src/components/LogoLink.astro +2 -1
  10. package/template/src/components/MdxPage.astro +15 -4
  11. package/template/src/components/PagePagination.astro +61 -0
  12. package/template/src/components/SidebarDropdown.astro +12 -8
  13. package/template/src/components/SidebarGroup.astro +1 -1
  14. package/template/src/components/SidebarMenu.astro +1 -1
  15. package/template/src/components/SidebarSegmented.astro +6 -5
  16. package/template/src/components/TableOfContents.astro +4 -13
  17. package/template/src/components/chat/AskAiWidget.tsx +274 -39
  18. package/template/src/components/endpoint/PlaygroundForm.astro +2 -1
  19. package/template/src/components/user/CodeBlock.astro +1 -1
  20. package/template/src/components/user/CodeGroup.astro +16 -1
  21. package/template/src/components/user/ComponentPreviewBlock.astro +1 -0
  22. package/template/src/components/user/Image.astro +43 -53
  23. package/template/src/layouts/Layout.astro +217 -7
  24. package/template/src/lib/base-path.ts +98 -0
  25. package/template/src/lib/component-error.ts +49 -10
  26. package/template/src/lib/mdx/remark-resolve-internal-links.ts +128 -18
  27. package/template/src/lib/pagefind.ts +62 -14
  28. package/template/src/lib/routes.ts +49 -1
  29. package/template/src/lib/static-asset-url.ts +3 -1
  30. package/template/src/lib/utils.ts +12 -4
  31. package/template/src/lib/validation.ts +376 -36
  32. package/template/src/pages/404.astro +2 -1
  33. package/template/src/pages/[...slug].astro +68 -6
  34. package/template/src/styles/global.css +62 -1
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
3
  import type { Link, Root } from "mdast";
3
4
  import type { Plugin } from "unified";
@@ -6,15 +7,17 @@ import {
6
7
  getConfig,
7
8
  loadOpenApiSpec,
8
9
  type DocsConfig,
10
+ type HiddenPageRoute,
9
11
  type NavGroup,
10
12
  type NavMenuItem,
11
13
  type NavOpenApi,
12
14
  type NavOpenApiPage,
13
15
  type NavPage,
14
16
  } from "../validation";
17
+ import { prependBasePath, withBasePath } from "../base-path";
15
18
  import {
16
19
  buildMdxPageHref,
17
- buildOpenApiEndpointSlug,
20
+ buildOpenApiEndpointHref,
18
21
  parseOpenApiEndpoint,
19
22
  slugify,
20
23
  } from "../utils";
@@ -25,6 +28,7 @@ type ResolvedRouteIndex = {
25
28
  canonicalHrefByFilePath: Map<string, string>;
26
29
  allHrefsByFilePath: Map<string, string[]>;
27
30
  validRoutePaths: Set<string>;
31
+ allDocsFilePaths: Set<string>;
28
32
  };
29
33
 
30
34
  type LinkSplit = {
@@ -145,6 +149,38 @@ function getCurrentDocFilePath(filePath: string | undefined): string | null {
145
149
  return normalizeDocsFilePath(relativePath);
146
150
  }
147
151
 
152
+ function collectDocsFilePaths(directory: string): string[] {
153
+ let entries: fs.Dirent[];
154
+ try {
155
+ entries = fs.readdirSync(directory, { withFileTypes: true });
156
+ } catch {
157
+ return [];
158
+ }
159
+
160
+ const results: string[] = [];
161
+ for (const entry of entries) {
162
+ const entryPath = path.join(directory, entry.name);
163
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
164
+ continue;
165
+ }
166
+
167
+ if (entry.isDirectory()) {
168
+ results.push(...collectDocsFilePaths(entryPath));
169
+ continue;
170
+ }
171
+
172
+ if (entry.isFile() && /\.(md|mdx)$/i.test(entry.name)) {
173
+ const relativePath = path.relative(DOCS_ROOT, entryPath);
174
+ const normalizedFilePath = normalizeDocsFilePath(relativePath);
175
+ if (normalizedFilePath) {
176
+ results.push(normalizedFilePath);
177
+ }
178
+ }
179
+ }
180
+
181
+ return results;
182
+ }
183
+
148
184
  function buildFilePathCandidates(args: {
149
185
  baseHref: string;
150
186
  currentDocFilePath: string | null;
@@ -235,11 +271,11 @@ function addMdxRoute(args: {
235
271
  }
236
272
 
237
273
  if (args.homePath && normalizedFilePath === args.homePath) {
238
- addValidRoutePath(args.index, "/");
239
- if (!aliases.includes("/")) {
240
- aliases.unshift("/");
274
+ const homeHref = addValidRoutePath(args.index, prependBasePath("/"));
275
+ if (!aliases.includes(homeHref)) {
276
+ aliases.unshift(homeHref);
241
277
  }
242
- args.index.canonicalHrefByFilePath.set(normalizedFilePath, "/");
278
+ args.index.canonicalHrefByFilePath.set(normalizedFilePath, homeHref);
243
279
  }
244
280
 
245
281
  args.index.allHrefsByFilePath.set(normalizedFilePath, aliases);
@@ -251,11 +287,33 @@ function addOpenApiEndpointRoute(args: {
251
287
  method: string;
252
288
  endpointPath: string;
253
289
  }): 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);
290
+ addValidRoutePath(
291
+ args.index,
292
+ buildOpenApiEndpointHref({
293
+ path: args.endpointPath,
294
+ method: args.method,
295
+ groupSlug: args.parentSlug,
296
+ }),
297
+ );
298
+ }
299
+
300
+ function addHiddenPageRoute(
301
+ index: ResolvedRouteIndex,
302
+ route: HiddenPageRoute,
303
+ ): void {
304
+ const normalizedFilePath = normalizeDocsFilePath(route.filePath);
305
+ if (!normalizedFilePath) return;
306
+
307
+ const href = addValidRoutePath(index, prependBasePath(route.href));
308
+ if (!index.canonicalHrefByFilePath.has(normalizedFilePath)) {
309
+ index.canonicalHrefByFilePath.set(normalizedFilePath, href);
310
+ }
311
+
312
+ const aliases = index.allHrefsByFilePath.get(normalizedFilePath) ?? [];
313
+ if (!aliases.includes(href)) {
314
+ aliases.push(href);
315
+ }
316
+ index.allHrefsByFilePath.set(normalizedFilePath, aliases);
259
317
  }
260
318
 
261
319
  function shouldIncludeEndpoint(
@@ -431,13 +489,20 @@ async function processMenuItems(args: {
431
489
  }
432
490
  }
433
491
 
434
- async function buildRouteIndex(config: DocsConfig): Promise<ResolvedRouteIndex> {
492
+ async function buildRouteIndex(
493
+ config: DocsConfig,
494
+ ): Promise<ResolvedRouteIndex> {
435
495
  const index: ResolvedRouteIndex = {
436
496
  canonicalHrefByFilePath: new Map<string, string>(),
437
497
  allHrefsByFilePath: new Map<string, string[]>(),
438
498
  validRoutePaths: new Set<string>(),
499
+ allDocsFilePaths: new Set<string>(),
439
500
  };
440
501
 
502
+ for (const docFilePath of collectDocsFilePaths(DOCS_ROOT)) {
503
+ index.allDocsFilePaths.add(docFilePath);
504
+ }
505
+
441
506
  const homePath =
442
507
  typeof config.home === "string" ? normalizeDocsFilePath(config.home) : null;
443
508
 
@@ -459,7 +524,10 @@ async function buildRouteIndex(config: DocsConfig): Promise<ResolvedRouteIndex>
459
524
  }
460
525
 
461
526
  const rootOpenApi = (config.navigation as any).openapi;
462
- if (typeof rootOpenApi === "string" || (rootOpenApi && typeof rootOpenApi === "object")) {
527
+ if (
528
+ typeof rootOpenApi === "string" ||
529
+ (rootOpenApi && typeof rootOpenApi === "object")
530
+ ) {
463
531
  await processOpenApiFile({
464
532
  index,
465
533
  parentSlug: "",
@@ -467,7 +535,10 @@ async function buildRouteIndex(config: DocsConfig): Promise<ResolvedRouteIndex>
467
535
  });
468
536
  } else if (Array.isArray(rootOpenApi)) {
469
537
  for (const openApiItem of rootOpenApi) {
470
- if (typeof openApiItem !== "string" && (!openApiItem || typeof openApiItem !== "object")) {
538
+ if (
539
+ typeof openApiItem !== "string" &&
540
+ (!openApiItem || typeof openApiItem !== "object")
541
+ ) {
471
542
  continue;
472
543
  }
473
544
 
@@ -480,7 +551,11 @@ async function buildRouteIndex(config: DocsConfig): Promise<ResolvedRouteIndex>
480
551
  }
481
552
 
482
553
  if (homePath) {
483
- addValidRoutePath(index, "/");
554
+ addValidRoutePath(index, prependBasePath("/"));
555
+ }
556
+
557
+ for (const hiddenPageRoute of config.hiddenPageRoutes ?? []) {
558
+ addHiddenPageRoute(index, hiddenPageRoute);
484
559
  }
485
560
 
486
561
  return index;
@@ -501,11 +576,24 @@ function buildInvalidInternalLinkError(args: {
501
576
  baseHref: string;
502
577
  currentDocFilePath: string | null;
503
578
  filePathCandidates: string[];
579
+ existingFilePathCandidates: string[];
504
580
  }): Error {
505
581
  const sourceFile = args.currentDocFilePath
506
582
  ? `${args.currentDocFilePath}.mdx`
507
583
  : "the current MDX file";
508
584
 
585
+ if (args.existingFilePathCandidates.length > 0) {
586
+ const existingFiles = args.existingFilePathCandidates
587
+ .map((candidate) => `"${candidate}.mdx"`)
588
+ .join(", ");
589
+
590
+ return new Error(
591
+ `[USER_ERROR]: Invalid internal link "${args.baseHref}" in ${sourceFile}. ` +
592
+ `The target file exists (${existingFiles}), but it is not a routable docs page. ` +
593
+ `Add it to docs.json navigation, set it as the home page, or link to it from the navbar or footer so it is included in the built site.`,
594
+ );
595
+ }
596
+
509
597
  const candidateHint =
510
598
  args.filePathCandidates.length > 0
511
599
  ? ` Tried file path candidates: ${args.filePathCandidates
@@ -515,7 +603,7 @@ function buildInvalidInternalLinkError(args: {
515
603
 
516
604
  return new Error(
517
605
  `[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}`,
606
+ `No matching docs page file was found. Create the target .mdx file or update the link.${candidateHint}`,
519
607
  );
520
608
  }
521
609
 
@@ -532,12 +620,29 @@ function resolveLinkToCanonicalHref(args: {
532
620
  }
533
621
 
534
622
  const normalizedBaseRoute = normalizeRoutePath(baseHref);
623
+ const normalizedBaseRouteWithBase = normalizeRoutePath(
624
+ baseHref.startsWith("/")
625
+ ? withBasePath(normalizedBaseRoute)
626
+ : prependBasePath(normalizedBaseRoute),
627
+ );
628
+ if (args.routeIndex.validRoutePaths.has(normalizedBaseRouteWithBase)) {
629
+ if (
630
+ baseHref.startsWith("/") &&
631
+ normalizedBaseRoute === normalizedBaseRouteWithBase
632
+ ) {
633
+ return null;
634
+ }
635
+
636
+ return `${normalizedBaseRouteWithBase}${parts.suffix}`;
637
+ }
638
+
535
639
  if (args.routeIndex.validRoutePaths.has(normalizedBaseRoute)) {
536
- if (baseHref.startsWith("/")) {
640
+ const publicRoute = normalizeRoutePath(withBasePath(normalizedBaseRoute));
641
+ if (baseHref.startsWith("/") && normalizedBaseRoute === publicRoute) {
537
642
  return null;
538
643
  }
539
644
 
540
- return `${normalizedBaseRoute}${parts.suffix}`;
645
+ return `${publicRoute}${parts.suffix}`;
541
646
  }
542
647
 
543
648
  const filePathCandidates = buildFilePathCandidates({
@@ -556,14 +661,19 @@ function resolveLinkToCanonicalHref(args: {
556
661
  );
557
662
  }
558
663
 
559
- return `${canonicalHref}${parts.suffix}`;
664
+ return `${withBasePath(canonicalHref)}${parts.suffix}`;
560
665
  }
561
666
 
562
667
  if (shouldEnforceInternalRouteValidation(baseHref)) {
668
+ const existingFilePathCandidates = filePathCandidates.filter((filePath) =>
669
+ args.routeIndex.allDocsFilePaths.has(filePath),
670
+ );
671
+
563
672
  throw buildInvalidInternalLinkError({
564
673
  baseHref,
565
674
  currentDocFilePath: args.currentDocFilePath,
566
675
  filePathCandidates,
676
+ existingFilePathCandidates,
567
677
  });
568
678
  }
569
679
 
@@ -1,4 +1,5 @@
1
1
  // Pagefind TypeScript types and wrapper
2
+ import { getDocsBasePath, withBasePath } from "./base-path";
2
3
  import { resolveStaticAssetUrl } from "./static-asset-url";
3
4
 
4
5
  export interface PagefindSearchResult {
@@ -38,11 +39,11 @@ export interface PagefindSearchResponse {
38
39
  }
39
40
 
40
41
  export interface PagefindInstance {
41
- options?: (options: { baseUrl?: string; basePath?: string }) => Promise<void>;
42
+ options?: (options: { baseUrl?: string }) => Promise<void>;
42
43
  init: () => Promise<void>;
43
44
  search: (
44
45
  query: string,
45
- options?: { filters?: Record<string, string> }
46
+ options?: { filters?: Record<string, string> },
46
47
  ) => Promise<PagefindSearchResponse>;
47
48
  filters: () => Promise<Record<string, Record<string, number>>>;
48
49
  preload: (query: string) => Promise<void>;
@@ -54,6 +55,57 @@ function getPagefindScriptUrl(): string {
54
55
  return resolveStaticAssetUrl("/pagefind/pagefind.js");
55
56
  }
56
57
 
58
+ function getPagefindResultBaseUrl(): string {
59
+ if (typeof window === "undefined") return "/";
60
+
61
+ return `${window.location.origin}${getDocsBasePath()}`;
62
+ }
63
+
64
+ function withBasePathForSearchResultUrl(url: string): string {
65
+ const basePath = getDocsBasePath();
66
+ if (!basePath || typeof window === "undefined") {
67
+ return withBasePath(url);
68
+ }
69
+
70
+ const isAbsoluteUrl =
71
+ /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url) || url.startsWith("//");
72
+ if (!isAbsoluteUrl) {
73
+ return withBasePath(url);
74
+ }
75
+
76
+ try {
77
+ const parsed = new URL(url, window.location.href);
78
+ if (
79
+ parsed.origin === window.location.origin &&
80
+ parsed.pathname !== basePath &&
81
+ !parsed.pathname.startsWith(`${basePath}/`)
82
+ ) {
83
+ const pathname = parsed.pathname.startsWith("/")
84
+ ? parsed.pathname
85
+ : `/${parsed.pathname}`;
86
+ parsed.pathname = `${basePath}${pathname}`;
87
+ return parsed.toString();
88
+ }
89
+ } catch {
90
+ // Fall through to normal relative URL handling.
91
+ }
92
+
93
+ return withBasePath(url);
94
+ }
95
+
96
+ function withBasePathForSearchResult(
97
+ data: PagefindResultData,
98
+ ): PagefindResultData {
99
+ return {
100
+ ...data,
101
+ url: withBasePathForSearchResultUrl(data.url),
102
+ sub_results: data.sub_results?.map((subResult) => ({
103
+ ...subResult,
104
+ url: withBasePathForSearchResultUrl(subResult.url),
105
+ })),
106
+ };
107
+ }
108
+
57
109
  export async function getPagefind(): Promise<PagefindInstance | null> {
58
110
  if (pagefindInstance) return pagefindInstance;
59
111
 
@@ -62,17 +114,15 @@ export async function getPagefind(): Promise<PagefindInstance | null> {
62
114
  // This keeps Pagefind loading purely runtime and allows versioned query params.
63
115
  const importPagefind = new Function(
64
116
  "moduleUrl",
65
- "return import(moduleUrl)"
117
+ "return import(moduleUrl)",
66
118
  ) as (moduleUrl: string) => Promise<PagefindInstance>;
67
119
 
68
120
  const pagefind = await importPagefind(getPagefindScriptUrl());
69
- // Pagefind uses the script import location to derive result URL base.
70
- // Since we import from the static host, force result links back to the
71
- // current docs site origin while leaving asset fetches on static host.
121
+ // Pagefind uses the pagefind.js import URL to locate its index files.
122
+ // Only override result URLs; passing Pagefind's basePath would move
123
+ // pagefind-entry.json and chunk fetches back onto the docs origin.
72
124
  if (typeof pagefind.options === "function") {
73
- const baseUrl =
74
- typeof window !== "undefined" ? window.location.origin : "/";
75
- await pagefind.options({ baseUrl });
125
+ await pagefind.options({ baseUrl: getPagefindResultBaseUrl() });
76
126
  }
77
127
  await pagefind.init();
78
128
  pagefindInstance = pagefind;
@@ -85,7 +135,7 @@ export async function getPagefind(): Promise<PagefindInstance | null> {
85
135
 
86
136
  export async function search(
87
137
  query: string,
88
- limit: number = 8
138
+ limit: number = 8,
89
139
  ): Promise<PagefindResultData[]> {
90
140
  const pagefind = await getPagefind();
91
141
  if (!pagefind || !query.trim()) return [];
@@ -94,10 +144,8 @@ export async function search(
94
144
 
95
145
  // Load full data for top results
96
146
  const results = await Promise.all(
97
- response.results.slice(0, limit).map((result) => result.data())
147
+ response.results.slice(0, limit).map((result) => result.data()),
98
148
  );
99
149
 
100
- console.log("results", results);
101
-
102
- return results;
150
+ return results.map(withBasePathForSearchResult);
103
151
  }
@@ -5,6 +5,7 @@ import {
5
5
  type NavOpenApiPage,
6
6
  type NavMenuItem,
7
7
  type NavOpenApi,
8
+ type HiddenPageRoute,
8
9
  loadOpenApiSpec,
9
10
  } from "./validation";
10
11
  import {
@@ -21,6 +22,7 @@ type MdxNavPageItem = string | NavPage;
21
22
  export interface BaseRoute {
22
23
  slug: string;
23
24
  title: string;
25
+ hidden?: boolean;
24
26
  }
25
27
 
26
28
  // MDX route
@@ -95,6 +97,35 @@ function processPageItem(
95
97
  };
96
98
  }
97
99
 
100
+ function processHiddenPageRoute(
101
+ route: HiddenPageRoute,
102
+ docs: any[],
103
+ ): MdxRoute {
104
+ const entry = docs.find((doc: any) => {
105
+ const docPath = doc.id.replace(/\.mdx$/, "");
106
+ return docPath === route.filePath;
107
+ });
108
+
109
+ if (!entry) {
110
+ throw new Error(
111
+ `Could not find content collection entry for path: ${route.filePath}`,
112
+ );
113
+ }
114
+
115
+ const slug = route.href.replace(/^\/+/, "").replace(/\/+$/, "");
116
+
117
+ return {
118
+ type: "mdx",
119
+ slug,
120
+ filePath: route.filePath,
121
+ title: resolveMdxPageTitle({
122
+ entry,
123
+ filePath: route.filePath,
124
+ }),
125
+ hidden: true,
126
+ };
127
+ }
128
+
98
129
  type OpenApiOperationLookup = {
99
130
  method: string;
100
131
  path: string;
@@ -368,7 +399,7 @@ function assertUniqueRouteSlugs(routes: Route[]): void {
368
399
  : `openapi:${route.filePath}:${route.openApiMethod.toUpperCase()} ${route.openApiPath}`;
369
400
 
370
401
  throw new Error(
371
- `Duplicate route slug "${route.slug}" generated by "${existingLabel}" and "${candidateLabel}".`,
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.`,
372
403
  );
373
404
  }
374
405
  }
@@ -405,6 +436,23 @@ export async function getAllRoutes(): Promise<Route[]> {
405
436
  }
406
437
  }
407
438
 
439
+ for (const hiddenPageRoute of config.hiddenPageRoutes ?? []) {
440
+ const hiddenRoute = processHiddenPageRoute(hiddenPageRoute, docs);
441
+ const existingRoute = allRoutes.find(
442
+ (route) => route.slug === hiddenRoute.slug,
443
+ );
444
+ if (existingRoute) {
445
+ if (
446
+ existingRoute.type === "mdx" &&
447
+ existingRoute.filePath === hiddenRoute.filePath
448
+ ) {
449
+ continue;
450
+ }
451
+ }
452
+
453
+ allRoutes.push(hiddenRoute);
454
+ }
455
+
408
456
  assertUniqueRouteSlugs(allRoutes);
409
457
  return allRoutes;
410
458
  }
@@ -1,3 +1,5 @@
1
+ import { withBasePath } from "./base-path";
2
+
1
3
  type AssetsPrefixValue = string | Record<string, string> | undefined;
2
4
 
3
5
  function normalizePrefix(value: string): string {
@@ -54,7 +56,7 @@ export function resolveStaticAssetUrl(rawPath: string): string {
54
56
 
55
57
  const prefix = resolveAssetsPrefix(import.meta.env.ASSETS_PREFIX);
56
58
  if (!prefix) {
57
- return `${normalizedPathname}${parsed.search}${parsed.hash}`;
59
+ return `${withBasePath(normalizedPathname)}${parsed.search}${parsed.hash}`;
58
60
  }
59
61
 
60
62
  const normalizedPrefixPath = `${prefix}/${normalizedPathname.replace(/^\/+/, "")}`;
@@ -5,6 +5,7 @@ import remarkRehype from "remark-rehype";
5
5
  import rehypeStringify from "rehype-stringify";
6
6
  import rehypeExternalLinks from "./mdx/rehype-external-links";
7
7
  import path from "node:path";
8
+ import { prependBasePath } from "./base-path";
8
9
 
9
10
  export function slugify(text: string): string {
10
11
  if (typeof text !== "string") {
@@ -66,7 +67,7 @@ export function buildMdxPageHref(args: {
66
67
  homePath?: string;
67
68
  }): string {
68
69
  if (args.homePath && args.filePath === args.homePath) {
69
- return "/";
70
+ return prependBasePath("/");
70
71
  }
71
72
 
72
73
  const filename = path.basename(args.filePath);
@@ -75,9 +76,11 @@ export function buildMdxPageHref(args: {
75
76
  .replace(/^\/+/, "")
76
77
  .replace(/\/+$/, "");
77
78
 
78
- return normalizedGroupSlug
79
+ const href = normalizedGroupSlug
79
80
  ? `/${normalizedGroupSlug}/${pageSlug}`
80
81
  : `/${pageSlug}`;
82
+
83
+ return prependBasePath(href);
81
84
  }
82
85
 
83
86
  export function parseOpenApiEndpoint(
@@ -103,7 +106,10 @@ export function parseOpenApiEndpoint(
103
106
  };
104
107
  }
105
108
 
106
- export function buildOpenApiEndpointSlug(pathStr: string, method: string): string {
109
+ export function buildOpenApiEndpointSlug(
110
+ pathStr: string,
111
+ method: string,
112
+ ): string {
107
113
  const normalizedPath = pathStr.startsWith("/") ? pathStr : `/${pathStr}`;
108
114
  const pathSlug = normalizedPath
109
115
  .replace(/^\//, "")
@@ -123,7 +129,9 @@ export function buildOpenApiEndpointHref(args: {
123
129
  .replace(/^\/+/, "")
124
130
  .replace(/\/+$/, "");
125
131
 
126
- return normalizedGroupSlug
132
+ const href = normalizedGroupSlug
127
133
  ? `/${normalizedGroupSlug}/${endpointSlug}`
128
134
  : `/${endpointSlug}`;
135
+
136
+ return prependBasePath(href);
129
137
  }