radiant-docs 0.1.66 → 0.1.67

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "radiant-docs",
3
- "version": "0.1.66",
3
+ "version": "0.1.67",
4
4
  "description": "CLI tool for previewing Radiant documentation locally",
5
5
  "type": "module",
6
6
  "bin": {
@@ -311,6 +311,24 @@ function resolveDocsSiteConfig() {
311
311
  const docsSiteConfig = resolveDocsSiteConfig();
312
312
  globalThis.__RADIANT_DOCS_BASE_PATH__ = docsSiteConfig.base;
313
313
 
314
+ function isInternalDocsRoutePathname(pathname) {
315
+ const basePath = docsSiteConfig.base === "/" ? "" : docsSiteConfig.base;
316
+ const relativePath =
317
+ basePath && pathname.startsWith(`${basePath}/`)
318
+ ? pathname.slice(basePath.length)
319
+ : pathname;
320
+
321
+ return relativePath === "/-" || relativePath.startsWith("/-/");
322
+ }
323
+
324
+ function shouldIncludeSitemapPage(page) {
325
+ try {
326
+ return !isInternalDocsRoutePathname(new URL(page).pathname);
327
+ } catch {
328
+ return !page.startsWith("/-/");
329
+ }
330
+ }
331
+
314
332
  const markdownProcessor = unified({
315
333
  remarkPlugins: [
316
334
  remarkGfm,
@@ -387,7 +405,9 @@ export default defineConfig({
387
405
  assetsPrefix: configuredAssetsPrefix,
388
406
  },
389
407
  integrations: [
390
- sitemap(),
408
+ sitemap({
409
+ filter: shouldIncludeSitemapPage,
410
+ }),
391
411
  preact({
392
412
  compat: true,
393
413
  }),
@@ -28,7 +28,7 @@
28
28
  "@fontsource-variable/source-serif-4": "^5.2.9",
29
29
  "@iconify-json/fluent": "^1.2.47",
30
30
  "@iconify-json/lucide": "^1.2.79",
31
- "@iconify-json/simple-icons": "^1.2.69",
31
+ "@iconify-json/simple-icons": "^1.2.87",
32
32
  "@iconify/react": "^6.0.2",
33
33
  "@paper-design/shaders": "^0.0.76",
34
34
  "@preact/preset-vite": "^2.10.3",
@@ -2343,9 +2343,9 @@
2343
2343
  }
2344
2344
  },
2345
2345
  "node_modules/@iconify-json/simple-icons": {
2346
- "version": "1.2.69",
2347
- "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.69.tgz",
2348
- "integrity": "sha512-T/rhy5n7pzE0ZOxQVlF68SNPCYYjRBpddjgjrJO5WWVRG8es5BQmvxIE9kKF+t2hhPGvuGQFpXmUyqbOtnxirQ==",
2346
+ "version": "1.2.87",
2347
+ "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.87.tgz",
2348
+ "integrity": "sha512-8YciStObhSji3OZFmWAWK6kBujyqO5bLCxeDwLxf3CR3F4PVelq7keC2LBvgTqviWzSTysj5/g4PCFLiAMVGsw==",
2349
2349
  "license": "CC0-1.0",
2350
2350
  "dependencies": {
2351
2351
  "@iconify/types": "*"
@@ -27,7 +27,7 @@
27
27
  "@fontsource-variable/source-serif-4": "^5.2.9",
28
28
  "@iconify-json/fluent": "^1.2.47",
29
29
  "@iconify-json/lucide": "^1.2.79",
30
- "@iconify-json/simple-icons": "^1.2.69",
30
+ "@iconify-json/simple-icons": "^1.2.87",
31
31
  "@iconify/react": "^6.0.2",
32
32
  "@paper-design/shaders": "^0.0.76",
33
33
  "@preact/preset-vite": "^2.10.3",
@@ -19,6 +19,7 @@ import ComponentPreview from "./user/ComponentPreview.astro";
19
19
  import ComponentPreviewBlock from "./user/ComponentPreviewBlock.astro";
20
20
  import type { MdxRoute, Route } from "../lib/routes";
21
21
  import PagePagination from "./PagePagination.astro";
22
+ import PageAiActions from "./PageAiActions.astro";
22
23
  import { PREVIEW_HEADING_ID_PREFIX } from "../lib/mdx/rehype-prefix-preview-heading-ids";
23
24
 
24
25
  interface Props {
@@ -67,10 +68,19 @@ const tocHeadings = headings.filter(
67
68
  <Layout pageTitle={title} pageDescription={description}>
68
69
  <div class="flex w-full min-w-0 justify-between gap-x-10">
69
70
  <div class="mx-auto max-w-3xl w-full">
70
- <header class="mb-6">
71
- <h1 class="rd-document-heading text-4xl font-semibold tracking-tight">
71
+ <header
72
+ class="mb-8 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between"
73
+ >
74
+ <h1
75
+ class="rd-document-heading min-w-0 text-4xl font-semibold tracking-tight"
76
+ >
72
77
  {title}
73
78
  </h1>
79
+ <PageAiActions
80
+ title={title}
81
+ routeSlug={route.slug}
82
+ description={description}
83
+ />
74
84
  </header>
75
85
  <article class="prose-rules">
76
86
  <Content components={components} />
@@ -10,6 +10,7 @@ import ResponseFieldTree from "./endpoint/ResponseFieldTree.astro";
10
10
  import PlaygroundBar from "./endpoint/PlaygroundBar.astro";
11
11
  import PlaygroundForm from "./endpoint/PlaygroundForm.astro";
12
12
  import PlaygroundButton from "./endpoint/PlaygroundButton.astro";
13
+ import PageAiActions from "./PageAiActions.astro";
13
14
  import { getConfig } from "../lib/validation";
14
15
  import {
15
16
  getOpenApiOperationDoc,
@@ -59,11 +60,20 @@ const snippetStickyClass = hasTopbarNavigationTabs
59
60
  ---
60
61
 
61
62
  <Layout pageTitle={title}>
62
- <article>
63
- <header class="mb-6">
64
- <h1 class="rd-document-heading text-4xl font-semibold tracking-tight">
63
+ <article class="mx-auto w-full max-w-7xl">
64
+ <header
65
+ class="mb-8 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between"
66
+ >
67
+ <h1
68
+ class="rd-document-heading min-w-0 text-4xl font-semibold tracking-tight"
69
+ >
65
70
  {title}
66
71
  </h1>
72
+ <PageAiActions
73
+ title={title}
74
+ routeSlug={route.slug}
75
+ description={description}
76
+ />
67
77
  </header>
68
78
  <div class="flex flex-row-reverse justify-between gap-6 w-full">
69
79
  <aside class="flex-1 min-w-0 hidden xl:block">
@@ -0,0 +1,715 @@
1
+ ---
2
+ import { Buffer } from "node:buffer";
3
+ import Icon from "./ui/Icon.astro";
4
+ import { prependBasePath } from "../lib/base-path";
5
+
6
+ interface Props {
7
+ title: string;
8
+ routeSlug: string;
9
+ description?: string;
10
+ }
11
+
12
+ type MenuItem = {
13
+ type: "button" | "link";
14
+ icon: string;
15
+ title: string;
16
+ description: string;
17
+ href?: string;
18
+ target?: string;
19
+ rel?: string;
20
+ external?: boolean;
21
+ copyMarkdown?: boolean;
22
+ copyMcp?: string;
23
+ hasActionLabel?: boolean;
24
+ openApp?: string;
25
+ };
26
+
27
+ function getMarkdownPathForSlug(routeSlug: string): string {
28
+ const normalizedSlug = routeSlug.replace(/^\/+/, "").replace(/\/+$/, "");
29
+ return normalizedSlug ? `/${normalizedSlug}.md` : "/index.md";
30
+ }
31
+
32
+ function getPagePathForSlug(routeSlug: string): string {
33
+ const normalizedSlug = routeSlug.replace(/^\/+/, "").replace(/\/+$/, "");
34
+ return normalizedSlug ? `/${normalizedSlug}` : "/";
35
+ }
36
+
37
+ function getInstallNameFromUrl(url: string): string {
38
+ try {
39
+ const hostname = new URL(url).hostname;
40
+ const normalized = hostname
41
+ .toLowerCase()
42
+ .replace(/[^a-z0-9]+/g, "-")
43
+ .replace(/^-+|-+$/g, "")
44
+ .slice(0, 48);
45
+
46
+ return normalized || "docs";
47
+ } catch {
48
+ return "docs";
49
+ }
50
+ }
51
+
52
+ function getCursorInstallNameFromUrl(url: string): string {
53
+ try {
54
+ return new URL(url).hostname.toLowerCase() || "docs";
55
+ } catch {
56
+ return "docs";
57
+ }
58
+ }
59
+
60
+ function toBase64(value: string): string {
61
+ return Buffer.from(value, "utf8").toString("base64");
62
+ }
63
+
64
+ function buildAssistantPrompt(pageUrl: string): string {
65
+ return `Read from ${pageUrl} so I can ask questions about it.`;
66
+ }
67
+
68
+ const { routeSlug } = Astro.props;
69
+ const parsedOrgTier = Number.parseInt(
70
+ (import.meta.env.ORG_TIER ?? "1").toString(),
71
+ 10,
72
+ );
73
+ const orgTier =
74
+ Number.isFinite(parsedOrgTier) && parsedOrgTier > 0 ? parsedOrgTier : 1;
75
+ const isDev = import.meta.env.DEV;
76
+ const mcpEnabled = isDev || orgTier >= 3;
77
+ const markdownPath = prependBasePath(getMarkdownPathForSlug(routeSlug));
78
+ const pagePath = prependBasePath(getPagePathForSlug(routeSlug));
79
+ const pageUrl = new URL(pagePath, Astro.site ?? Astro.url).toString();
80
+ const mcpPath = prependBasePath("/_mcp/server");
81
+ const mcpUrl = new URL(mcpPath, Astro.site ?? Astro.url).toString();
82
+ const installName = getInstallNameFromUrl(mcpUrl);
83
+ const cursorInstallName = getCursorInstallNameFromUrl(mcpUrl);
84
+ const cursorServerConfig = {
85
+ name: cursorInstallName,
86
+ url: mcpUrl,
87
+ };
88
+ const cursorInstallUrl = `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(cursorInstallName)}&config=${encodeURIComponent(toBase64(JSON.stringify(cursorServerConfig)))}`;
89
+ const prompt = buildAssistantPrompt(pageUrl);
90
+ const chatGptUrl = `https://chatgpt.com/?q=${encodeURIComponent(prompt)}`;
91
+ const claudeUrl = `https://claude.ai/new?q=${encodeURIComponent(prompt)}`;
92
+
93
+ const menuItemClass =
94
+ "group cursor-pointer flex w-full items-center gap-2.5 rounded-lg p-1.5 text-left transition-colors hover:bg-neutral-100/80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-neutral-900 dark:hover:bg-neutral-900/80 dark:focus-visible:outline-white";
95
+ const menuIconClass =
96
+ "inline-flex size-[32px] shrink-0 items-center justify-center rounded-sm border border-neutral-900/10 text-neutral-500 dark:border-white/6 dark:text-neutral-400";
97
+ const menuTitleClass =
98
+ "block text-sm font-medium leading-5 text-neutral-900 dark:text-white";
99
+ const menuDescriptionClass =
100
+ "block text-xs leading-4 text-neutral-500 dark:text-neutral-400";
101
+
102
+ const menuItems: MenuItem[] = [
103
+ {
104
+ type: "button",
105
+ icon: "lucide:copy",
106
+ title: "Copy page",
107
+ description: "Copy page as Markdown for LLMs",
108
+ copyMarkdown: true,
109
+ hasActionLabel: true,
110
+ },
111
+ {
112
+ type: "link",
113
+ icon: "simple-icons:markdown",
114
+ title: "View as Markdown",
115
+ description: "Open the Markdown version of this page",
116
+ href: markdownPath,
117
+ target: "_blank",
118
+ rel: "noopener noreferrer",
119
+ external: true,
120
+ },
121
+ {
122
+ type: "link",
123
+ icon: "simple-icons:openai",
124
+ title: "Open in ChatGPT",
125
+ description: "Ask questions about this page",
126
+ href: chatGptUrl,
127
+ target: "_blank",
128
+ rel: "noopener noreferrer",
129
+ external: true,
130
+ },
131
+ {
132
+ type: "link",
133
+ icon: "simple-icons:claude",
134
+ title: "Open in Claude",
135
+ description: "Ask questions about this page",
136
+ href: claudeUrl,
137
+ target: "_blank",
138
+ rel: "noopener noreferrer",
139
+ external: true,
140
+ },
141
+ ...(mcpEnabled
142
+ ? [
143
+ {
144
+ type: "button",
145
+ icon: "lucide:terminal",
146
+ title: "Copy MCP install command",
147
+ description: "Copy command to install MCP server",
148
+ copyMcp: "install",
149
+ hasActionLabel: true,
150
+ },
151
+ {
152
+ type: "link",
153
+ icon: "simple-icons:cursor",
154
+ title: "Connect to Cursor",
155
+ description: "Install MCP Server on Cursor",
156
+ href: cursorInstallUrl,
157
+ openApp: "cursor",
158
+ external: true,
159
+ },
160
+ {
161
+ type: "button",
162
+ icon: "simple-icons:claudecode",
163
+ title: "Connect to Claude Code",
164
+ description: "Copy command for Claude Code",
165
+ copyMcp: "claudeCode",
166
+ hasActionLabel: true,
167
+ },
168
+ {
169
+ type: "button",
170
+ icon: "custom:codex",
171
+ title: "Connect to Codex",
172
+ description: "Copy command for Codex",
173
+ copyMcp: "codexCli",
174
+ hasActionLabel: true,
175
+ },
176
+ ]
177
+ : []),
178
+ ];
179
+ ---
180
+
181
+ <nav
182
+ class="relative flex w-max shrink-0 justify-end"
183
+ data-rd-page-actions
184
+ data-markdown-url={markdownPath}
185
+ data-mcp-url={mcpUrl}
186
+ data-install-name={installName}
187
+ aria-label="Page AI actions"
188
+ >
189
+ <div class="relative inline-flex w-max shrink-0 flex-col items-end">
190
+ <div
191
+ class="inline-flex w-max shrink-0 overflow-hidden rounded-lg border-[0.5px] border-neutral-900/12 bg-neutral-50/50 text-sm text-neutral-700 shadow-[0_.5px_1px_rgba(0,0,0,0.15),0_5px_12px_-6px_rgba(0,0,0,0.08)] dark:border-white/6 dark:bg-(--rd-code-surface) dark:text-neutral-200 dark:shadow-[0_-.5px_1px_rgba(255,255,255,0.15),0_5px_12px_-6px_rgba(0,0,0,0.2)]"
192
+ >
193
+ <button
194
+ type="button"
195
+ class="cursor-pointer inline-flex h-8 shrink-0 items-center gap-1.5 whitespace-nowrap pl-2.5 pr-2 transition-colors hover:bg-white/80 hover:text-neutral-950 focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-neutral-900 dark:hover:bg-white/6 dark:hover:text-white dark:focus-visible:outline-white"
196
+ data-rd-copy-markdown
197
+ >
198
+ <span
199
+ class="relative inline-flex size-3.5 shrink-0 items-center justify-center"
200
+ >
201
+ <Icon
202
+ name="lucide:copy"
203
+ class="size-3.5 origin-center scale-100 rotate-0 opacity-100 transition-all duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none"
204
+ data-rd-copy-icon
205
+ aria-hidden="true"
206
+ />
207
+ <Icon
208
+ name="lucide:check"
209
+ class="absolute size-3.5 stroke-3 origin-center scale-50 rotate-6 opacity-0 text-green-700/80 transition-all duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none dark:text-green-400/90"
210
+ data-rd-copy-check
211
+ aria-hidden="true"
212
+ />
213
+ <Icon
214
+ name="lucide:x"
215
+ class="absolute size-3.5 stroke-3 origin-center -rotate-6 scale-50 opacity-0 text-red-600/80 transition-all duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none dark:text-red-400/90"
216
+ data-rd-copy-error
217
+ aria-hidden="true"
218
+ />
219
+ </span>
220
+ <span data-rd-action-label>Copy page</span>
221
+ </button>
222
+
223
+ <button
224
+ type="button"
225
+ class="cursor-pointer inline-flex h-8 w-8 pr-0.5 items-center justify-center border-l-[0.5px] border-neutral-900/12 transition-colors hover:bg-white/80 hover:text-neutral-950 focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-neutral-900 dark:border-white/6 dark:hover:bg-white/6 dark:hover:text-white dark:focus-visible:outline-white"
226
+ data-rd-page-actions-trigger
227
+ aria-expanded="false"
228
+ aria-haspopup="true"
229
+ aria-label="More page actions"
230
+ >
231
+ <Icon
232
+ name="lucide:chevron-down"
233
+ class="size-3.5 transition-transform"
234
+ aria-hidden="true"
235
+ data-rd-page-actions-chevron
236
+ />
237
+ </button>
238
+ </div>
239
+
240
+ <div
241
+ class="rd-page-actions-dropdown absolute left-0 right-auto top-full z-30 mt-1 w-max max-w-[calc(100vw-2rem)] rounded-xl border-[0.5px] border-neutral-200 bg-white p-1 text-neutral-700 shadow-2xl shadow-neutral-900/12 sm:left-auto sm:right-0 dark:border-neutral-800 dark:bg-(--rd-code-surface) dark:text-neutral-200 dark:shadow-black/40"
242
+ data-origin="responsive"
243
+ data-rd-page-actions-menu
244
+ hidden
245
+ >
246
+ {
247
+ menuItems.map((item) =>
248
+ item.type === "button" ? (
249
+ <button
250
+ type="button"
251
+ class={menuItemClass}
252
+ data-rd-copy-markdown={item.copyMarkdown ? true : undefined}
253
+ data-rd-copy-mcp={item.copyMcp}
254
+ >
255
+ <span class={menuIconClass}>
256
+ {item.copyMarkdown || item.copyMcp ? (
257
+ <span class="relative inline-flex size-4 items-center justify-center">
258
+ {item.icon === "custom:codex" ? (
259
+ <svg
260
+ xmlns="http://www.w3.org/2000/svg"
261
+ viewBox="0 0 24 24"
262
+ fill="currentColor"
263
+ fill-rule="evenodd"
264
+ class="size-4 origin-center scale-100 rotate-0 opacity-100 transition-all duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none"
265
+ data-rd-copy-icon
266
+ aria-hidden="true"
267
+ >
268
+ <path
269
+ clip-rule="evenodd"
270
+ d="M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z"
271
+ />
272
+ </svg>
273
+ ) : (
274
+ <Icon
275
+ name={item.copyMarkdown ? "lucide:copy" : item.icon}
276
+ class="size-4 origin-center scale-100 rotate-0 opacity-100 transition-all duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none"
277
+ data-rd-copy-icon
278
+ aria-hidden="true"
279
+ />
280
+ )}
281
+ <Icon
282
+ name="lucide:check"
283
+ class="absolute size-4 stroke-3 origin-center scale-50 rotate-6 opacity-0 text-green-700/80 transition-all duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none dark:text-green-400/90"
284
+ data-rd-copy-check
285
+ aria-hidden="true"
286
+ />
287
+ <Icon
288
+ name="lucide:x"
289
+ class="absolute size-4 stroke-3 origin-center -rotate-6 scale-50 opacity-0 text-red-600/80 transition-all duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform motion-reduce:transition-none dark:text-red-400/90"
290
+ data-rd-copy-error
291
+ aria-hidden="true"
292
+ />
293
+ </span>
294
+ ) : item.icon === "custom:codex" ? (
295
+ <svg
296
+ xmlns="http://www.w3.org/2000/svg"
297
+ viewBox="0 0 24 24"
298
+ fill="currentColor"
299
+ fill-rule="evenodd"
300
+ class="size-4"
301
+ aria-hidden="true"
302
+ >
303
+ <path
304
+ clip-rule="evenodd"
305
+ d="M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z"
306
+ />
307
+ </svg>
308
+ ) : (
309
+ <Icon name={item.icon} class="size-4" aria-hidden="true" />
310
+ )}
311
+ </span>
312
+ <span class="min-w-0 pr-3">
313
+ <span
314
+ class={menuTitleClass}
315
+ data-rd-action-label={item.hasActionLabel ? true : undefined}
316
+ >
317
+ {item.title}
318
+ </span>
319
+ <span class={menuDescriptionClass}>{item.description}</span>
320
+ </span>
321
+ </button>
322
+ ) : (
323
+ <a
324
+ href={item.href}
325
+ target={item.target}
326
+ rel={item.rel}
327
+ class={menuItemClass}
328
+ data-rd-open-app={item.openApp}
329
+ >
330
+ <span class={menuIconClass}>
331
+ {item.icon === "custom:codex" ? (
332
+ <svg
333
+ xmlns="http://www.w3.org/2000/svg"
334
+ viewBox="0 0 24 24"
335
+ fill="currentColor"
336
+ fill-rule="evenodd"
337
+ class="size-4"
338
+ aria-hidden="true"
339
+ >
340
+ <path
341
+ clip-rule="evenodd"
342
+ d="M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z"
343
+ />
344
+ </svg>
345
+ ) : (
346
+ <Icon name={item.icon} class="size-4" aria-hidden="true" />
347
+ )}
348
+ </span>
349
+ <span class="min-w-0 pr-3">
350
+ <span class={menuTitleClass}>
351
+ {item.title}
352
+ {item.external && (
353
+ <Icon
354
+ name="lucide:arrow-up-right"
355
+ class="ml-[3px] mb-[3px] inline size-3 self-start"
356
+ aria-hidden="true"
357
+ />
358
+ )}
359
+ </span>
360
+ <span class={menuDescriptionClass}>{item.description}</span>
361
+ </span>
362
+ </a>
363
+ ),
364
+ )
365
+ }
366
+ </div>
367
+ </div>
368
+ </nav>
369
+
370
+ <style>
371
+ [data-rd-page-actions] {
372
+ --dropdown-open-dur: 250ms;
373
+ --dropdown-close-dur: 150ms;
374
+ --dropdown-pre-scale: 0.97;
375
+ --dropdown-closing-scale: 0.99;
376
+ --dropdown-ease: cubic-bezier(0.22, 1, 0.36, 1);
377
+ }
378
+
379
+ .rd-page-actions-dropdown {
380
+ transform-origin: top right;
381
+ transform: scale(var(--dropdown-pre-scale));
382
+ opacity: 0;
383
+ pointer-events: none;
384
+ transition:
385
+ transform var(--dropdown-open-dur) var(--dropdown-ease),
386
+ opacity var(--dropdown-open-dur) var(--dropdown-ease);
387
+ will-change: transform, opacity;
388
+ }
389
+
390
+ .rd-page-actions-dropdown[data-origin="top-left"] {
391
+ transform-origin: top left;
392
+ }
393
+
394
+ .rd-page-actions-dropdown[data-origin="top-center"] {
395
+ transform-origin: top center;
396
+ }
397
+
398
+ .rd-page-actions-dropdown[data-origin="top-right"] {
399
+ transform-origin: top right;
400
+ }
401
+
402
+ .rd-page-actions-dropdown[data-origin="responsive"] {
403
+ transform-origin: top left;
404
+ }
405
+
406
+ @media (min-width: 640px) {
407
+ .rd-page-actions-dropdown[data-origin="responsive"] {
408
+ transform-origin: top right;
409
+ }
410
+ }
411
+
412
+ .rd-page-actions-dropdown.is-open {
413
+ transform: scale(1);
414
+ opacity: 1;
415
+ pointer-events: auto;
416
+ }
417
+
418
+ .rd-page-actions-dropdown.is-closing {
419
+ transform: scale(var(--dropdown-closing-scale));
420
+ opacity: 0;
421
+ pointer-events: none;
422
+ transition:
423
+ transform var(--dropdown-close-dur) var(--dropdown-ease),
424
+ opacity var(--dropdown-close-dur) var(--dropdown-ease);
425
+ }
426
+
427
+ @media (prefers-reduced-motion: reduce) {
428
+ .rd-page-actions-dropdown {
429
+ transition: none !important;
430
+ }
431
+ }
432
+ </style>
433
+
434
+ <script is:inline data-astro-rerun>
435
+ (() => {
436
+ const script = document.currentScript;
437
+ let root = script?.previousElementSibling;
438
+ while (
439
+ root &&
440
+ (!(root instanceof HTMLElement) ||
441
+ !root.hasAttribute("data-rd-page-actions"))
442
+ ) {
443
+ root = root.previousElementSibling;
444
+ }
445
+
446
+ if (
447
+ !(root instanceof HTMLElement) ||
448
+ root.dataset.pageActionsBound === "true"
449
+ ) {
450
+ return;
451
+ }
452
+ root.dataset.pageActionsBound = "true";
453
+
454
+ const trigger = root.querySelector("[data-rd-page-actions-trigger]");
455
+ const menu = root.querySelector("[data-rd-page-actions-menu]");
456
+ const chevron = root.querySelector("[data-rd-page-actions-chevron]");
457
+ const markdownUrl = root.dataset.markdownUrl || "/index.md";
458
+ const mcpUrl = root.dataset.mcpUrl || "/_mcp/server";
459
+ const installName = root.dataset.installName || "docs";
460
+ const closeDurationMs = 150;
461
+ const copyFeedbackDurationMs = 1400;
462
+ let closeTimeout;
463
+ let mcpMetadataPromise;
464
+ const copyFeedbackTimeouts = new WeakMap();
465
+
466
+ function fallbackCopy(text) {
467
+ const textarea = document.createElement("textarea");
468
+ textarea.value = text;
469
+ textarea.setAttribute("readonly", "");
470
+ textarea.style.position = "fixed";
471
+ textarea.style.opacity = "0";
472
+ document.body.appendChild(textarea);
473
+ textarea.select();
474
+ const copied = document.execCommand("copy");
475
+ document.body.removeChild(textarea);
476
+ return copied;
477
+ }
478
+
479
+ async function copyToClipboard(text) {
480
+ try {
481
+ if (navigator.clipboard?.writeText) {
482
+ await navigator.clipboard.writeText(text);
483
+ return true;
484
+ }
485
+ } catch {
486
+ // Fallback below when clipboard API is unavailable or blocked.
487
+ }
488
+
489
+ return fallbackCopy(text);
490
+ }
491
+
492
+ function setExpanded(nextExpanded) {
493
+ if (!(trigger instanceof HTMLElement)) return;
494
+ trigger.setAttribute("aria-expanded", String(nextExpanded));
495
+ if (chevron instanceof HTMLElement) {
496
+ chevron.classList.toggle("rotate-180", nextExpanded);
497
+ }
498
+ }
499
+
500
+ function openMenu() {
501
+ if (!(menu instanceof HTMLElement)) return;
502
+ window.clearTimeout(closeTimeout);
503
+ menu.hidden = false;
504
+ menu.classList.remove("is-closing");
505
+ menu.classList.add("is-open");
506
+ setExpanded(true);
507
+ }
508
+
509
+ function closeMenu() {
510
+ if (!(menu instanceof HTMLElement) || menu.hidden) return;
511
+ window.clearTimeout(closeTimeout);
512
+ menu.classList.remove("is-open");
513
+ menu.classList.add("is-closing");
514
+ setExpanded(false);
515
+ closeTimeout = window.setTimeout(() => {
516
+ menu.classList.remove("is-closing");
517
+ menu.hidden = true;
518
+ }, closeDurationMs);
519
+ }
520
+
521
+ function toggleMenu() {
522
+ if (!(menu instanceof HTMLElement)) return;
523
+ if (menu.hidden || !menu.classList.contains("is-open")) {
524
+ openMenu();
525
+ } else {
526
+ closeMenu();
527
+ }
528
+ }
529
+
530
+ function setLabelFeedback(button, copiedText = "Copied") {
531
+ const label = button.querySelector("[data-rd-action-label]");
532
+ if (!(label instanceof HTMLElement)) return;
533
+
534
+ const defaultText = label.dataset.defaultLabel || label.textContent || "";
535
+ label.dataset.defaultLabel = defaultText;
536
+ label.textContent = copiedText;
537
+ window.setTimeout(() => {
538
+ label.textContent = label.dataset.defaultLabel || defaultText;
539
+ }, copyFeedbackDurationMs);
540
+ }
541
+
542
+ function setCopyIconState(button, state) {
543
+ const copyIcon = button.querySelector("[data-rd-copy-icon]");
544
+ const checkIcon = button.querySelector("[data-rd-copy-check]");
545
+ const errorIcon = button.querySelector("[data-rd-copy-error]");
546
+
547
+ if (!copyIcon || !checkIcon || !errorIcon) return false;
548
+
549
+ const showCopy = () => {
550
+ copyIcon.classList.remove("scale-50", "opacity-0", "-rotate-6");
551
+ copyIcon.classList.add("scale-100", "opacity-100", "rotate-0");
552
+ };
553
+
554
+ const hideCopy = () => {
555
+ copyIcon.classList.add("scale-50", "opacity-0", "-rotate-6");
556
+ copyIcon.classList.remove("scale-100", "opacity-100", "rotate-0");
557
+ };
558
+
559
+ const showStatusIcon = (icon) => {
560
+ icon.classList.remove("scale-50", "opacity-0", "rotate-6", "-rotate-6");
561
+ icon.classList.add("scale-110", "opacity-100", "rotate-0");
562
+ };
563
+
564
+ const hideStatusIcon = (icon, rotationClass) => {
565
+ icon.classList.remove("scale-110", "opacity-100", "rotate-0");
566
+ icon.classList.add("scale-50", "opacity-0", rotationClass);
567
+ };
568
+
569
+ if (state === "success") {
570
+ hideCopy();
571
+ showStatusIcon(checkIcon);
572
+ hideStatusIcon(errorIcon, "-rotate-6");
573
+ return true;
574
+ }
575
+
576
+ if (state === "error") {
577
+ hideCopy();
578
+ hideStatusIcon(checkIcon, "rotate-6");
579
+ showStatusIcon(errorIcon);
580
+ return true;
581
+ }
582
+
583
+ showCopy();
584
+ hideStatusIcon(checkIcon, "rotate-6");
585
+ hideStatusIcon(errorIcon, "-rotate-6");
586
+ return true;
587
+ }
588
+
589
+ function setCopyFeedback(button, state, fallbackText) {
590
+ if (setCopyIconState(button, state)) {
591
+ const existingTimeout = copyFeedbackTimeouts.get(button);
592
+ if (existingTimeout) window.clearTimeout(existingTimeout);
593
+
594
+ const timeout = window.setTimeout(() => {
595
+ setCopyIconState(button, "idle");
596
+ copyFeedbackTimeouts.delete(button);
597
+ }, copyFeedbackDurationMs);
598
+ copyFeedbackTimeouts.set(button, timeout);
599
+ return;
600
+ }
601
+
602
+ setLabelFeedback(
603
+ button,
604
+ fallbackText || (state === "error" ? "Failed" : "Copied"),
605
+ );
606
+ }
607
+
608
+ function getTomlServerKey() {
609
+ return (
610
+ installName.replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") ||
611
+ "docs"
612
+ );
613
+ }
614
+
615
+ function buildFallbackMcpMetadata() {
616
+ return {
617
+ usage: {
618
+ install: `npx add-mcp ${mcpUrl}`,
619
+ claudeCode: `claude mcp add --transport http ${installName} ${mcpUrl}`,
620
+ codexCli: `codex mcp add ${installName} --url ${mcpUrl}`,
621
+ cursor: `Add "${mcpUrl}" to your MCP server configuration`,
622
+ codex: `[mcp_servers.${getTomlServerKey()}]\nurl = "${mcpUrl}"`,
623
+ },
624
+ };
625
+ }
626
+
627
+ async function getMcpMetadata() {
628
+ if (!mcpMetadataPromise) {
629
+ mcpMetadataPromise = fetch(mcpUrl, {
630
+ headers: { Accept: "application/json" },
631
+ })
632
+ .then((response) => {
633
+ if (!response.ok) throw new Error("MCP metadata request failed");
634
+ return response.json();
635
+ })
636
+ .catch(() => buildFallbackMcpMetadata());
637
+ }
638
+
639
+ return mcpMetadataPromise;
640
+ }
641
+
642
+ async function copyMarkdown(button) {
643
+ const response = await fetch(markdownUrl, {
644
+ headers: { Accept: "text/markdown,text/plain;q=0.9,*/*;q=0.1" },
645
+ });
646
+ if (!response.ok) {
647
+ throw new Error("Markdown request failed");
648
+ }
649
+
650
+ const text = await response.text();
651
+ const copied = await copyToClipboard(text);
652
+ if (!copied) throw new Error("Clipboard write failed");
653
+ setCopyFeedback(button, "success");
654
+ }
655
+
656
+ async function copyMcpUsage(button) {
657
+ const key = button.getAttribute("data-rd-copy-mcp");
658
+ const metadata = await getMcpMetadata();
659
+ const usage = metadata?.usage || buildFallbackMcpMetadata().usage;
660
+ const fallbackUsage = buildFallbackMcpMetadata().usage;
661
+ const usageKey = key && key in fallbackUsage ? key : "cursor";
662
+ const value = usage[usageKey] || fallbackUsage[usageKey] || mcpUrl;
663
+
664
+ const copied = await copyToClipboard(value || mcpUrl);
665
+ if (!copied) throw new Error("Clipboard write failed");
666
+ setCopyFeedback(button, "success");
667
+ }
668
+
669
+ trigger?.addEventListener("click", (event) => {
670
+ event.stopPropagation();
671
+ toggleMenu();
672
+ });
673
+
674
+ menu?.addEventListener("click", (event) => {
675
+ event.stopPropagation();
676
+ });
677
+
678
+ document.addEventListener("click", (event) => {
679
+ if (!root.contains(event.target)) closeMenu();
680
+ });
681
+
682
+ document.addEventListener("keydown", (event) => {
683
+ if (event.key === "Escape") closeMenu();
684
+ });
685
+
686
+ root.querySelectorAll("[data-rd-copy-markdown]").forEach((button) => {
687
+ if (!(button instanceof HTMLButtonElement)) return;
688
+ button.addEventListener("click", async () => {
689
+ try {
690
+ await copyMarkdown(button);
691
+ } catch {
692
+ setCopyFeedback(button, "error", "Failed");
693
+ }
694
+ });
695
+ });
696
+
697
+ root.querySelectorAll("[data-rd-copy-mcp]").forEach((button) => {
698
+ if (!(button instanceof HTMLButtonElement)) return;
699
+ button.addEventListener("click", async () => {
700
+ try {
701
+ await copyMcpUsage(button);
702
+ } catch {
703
+ setCopyFeedback(button, "error", "Failed");
704
+ }
705
+ });
706
+ });
707
+
708
+ root.querySelectorAll("[data-rd-open-app]").forEach((link) => {
709
+ if (!(link instanceof HTMLAnchorElement)) return;
710
+ link.addEventListener("click", () => {
711
+ closeMenu();
712
+ });
713
+ });
714
+ })();
715
+ </script>
@@ -75,6 +75,8 @@ const navigationTabs = config.navigation.tabs as
75
75
  const hasNavigationTabs =
76
76
  Array.isArray(navigationTabs?.items) && navigationTabs.items.length > 0;
77
77
  const tabsPresentation = navigationTabs?.presentation ?? "topbar";
78
+ const hasTopbarNavigationTabs =
79
+ hasNavigationTabs && tabsPresentation === "topbar";
78
80
  ---
79
81
 
80
82
  <!doctype html>
@@ -356,7 +358,10 @@ const tabsPresentation = navigationTabs?.presentation ?? "topbar";
356
358
 
357
359
  <!-- Main Content -->
358
360
  <div
359
- class="mx-1 mt-1 px-4 sm:px-6 lg:pl-[calc(288px+32px)] pt-16 lg:pr-8 bg-background"
361
+ class:list={[
362
+ "mx-1 mt-1 px-4 sm:px-6 lg:pl-[calc(288px+32px)] pt-16 lg:pr-8 bg-background",
363
+ hasTopbarNavigationTabs && "lg:pt-[108px]",
364
+ ]}
360
365
  >
361
366
  <main class="mx-auto pt-16 pb-20 min-h-[calc(100vh-64px)]">
362
367
  <slot />
@@ -9,6 +9,7 @@ import {
9
9
  type MdxRoute,
10
10
  type OpenApiRoute,
11
11
  } from "./routes";
12
+ import { parseOpenApiEndpoint } from "./utils";
12
13
  import {
13
14
  getOpenApiOperationDoc,
14
15
  OPENAPI_REQUEST_SECTION_LABELS,
@@ -18,10 +19,19 @@ import {
18
19
  type OpenApiRequestSectionVariantData,
19
20
  } from "./openapi/operation-doc";
20
21
  import { resolvePageDescription } from "./page-description";
21
- import { getConfig } from "./validation";
22
+ import {
23
+ getConfig,
24
+ type DocsConfig,
25
+ type NavGroup,
26
+ type NavMenu,
27
+ type NavOpenApi,
28
+ type NavOpenApiPage,
29
+ type NavPage,
30
+ } from "./validation";
22
31
 
23
32
  const DOCS_ROOT = path.join(process.cwd(), "src/content/docs");
24
33
  const MAX_DESCRIPTION_LENGTH = 300;
34
+ const PRO_TIER = 3;
25
35
 
26
36
  const OPENAPI_SPEC_EXTENSIONS = new Set([".json", ".yaml", ".yml"]);
27
37
 
@@ -29,6 +39,7 @@ export const MARKDOWN_CONTENT_TYPE = "text/markdown; charset=utf-8";
29
39
  export const PLAIN_TEXT_CONTENT_TYPE = "text/plain; charset=utf-8";
30
40
 
31
41
  export type AiMarkdownPage = {
42
+ routeIdentity?: string;
32
43
  filePath: string;
33
44
  routeSlug?: string;
34
45
  title: string;
@@ -90,6 +101,24 @@ function buildPublicUrl(pathname: string): string {
90
101
  return new URL(publicPath, siteOrigin).toString();
91
102
  }
92
103
 
104
+ function getOrgTier(): number {
105
+ const parsed = Number.parseInt((process.env.ORG_TIER ?? "1").trim(), 10);
106
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
107
+ }
108
+
109
+ function formatMcpSection(): string[] {
110
+ if (getOrgTier() < PRO_TIER) return [];
111
+
112
+ return [
113
+ "## MCP",
114
+ "",
115
+ "This documentation exposes a read-only MCP server for AI agents:",
116
+ "",
117
+ `- MCP server: ${buildPublicUrl("/_mcp/server")}`,
118
+ "- Tool: searchDocs(query)",
119
+ ];
120
+ }
121
+
93
122
  function getMarkdownPathForSlug(routeSlug: string): string {
94
123
  const normalizedSlug = routeSlug.replace(/^\/+/, "").replace(/\/+$/, "");
95
124
  return normalizedSlug ? `/${normalizedSlug}.md` : "/index.md";
@@ -142,19 +171,23 @@ async function getMdxEntryByFilePath(filePath: string) {
142
171
  async function createMarkdownPage(args: {
143
172
  filePath: string;
144
173
  routeSlug?: string;
174
+ routeIdentity?: string;
145
175
  title?: string;
146
176
  markdownPath?: string;
147
177
  canonicalPath?: string;
148
178
  }): Promise<AiMarkdownPage> {
149
179
  const entry = await getMdxEntryByFilePath(args.filePath);
150
180
  if (!entry) {
151
- throw new Error(`Could not find content collection entry for "${args.filePath}".`);
181
+ throw new Error(
182
+ `Could not find content collection entry for "${args.filePath}".`,
183
+ );
152
184
  }
153
185
 
154
186
  const { sourcePath, source } = await readMdxSource(args.filePath);
155
187
  const routeSlug = args.routeSlug ?? "";
156
188
  const markdownPath = args.markdownPath ?? getMarkdownPathForSlug(routeSlug);
157
- const canonicalPath = args.canonicalPath ?? getCanonicalPathForSlug(routeSlug);
189
+ const canonicalPath =
190
+ args.canonicalPath ?? getCanonicalPathForSlug(routeSlug);
158
191
  const title =
159
192
  args.title ??
160
193
  resolveMdxPageTitle({
@@ -164,6 +197,7 @@ async function createMarkdownPage(args: {
164
197
  const config = await getConfig();
165
198
 
166
199
  return {
200
+ routeIdentity: args.routeIdentity,
167
201
  filePath: normalizeDocsRootRelativePath(args.filePath),
168
202
  routeSlug,
169
203
  title,
@@ -205,6 +239,7 @@ export async function getLlmsListedMarkdownPages(): Promise<AiMarkdownPage[]> {
205
239
  await createMarkdownPage({
206
240
  filePath: config.home,
207
241
  routeSlug: "",
242
+ routeIdentity: homeRoute?.routeIdentity,
208
243
  title: homeRoute?.title,
209
244
  markdownPath: "/index.md",
210
245
  canonicalPath: "/",
@@ -277,6 +312,234 @@ function formatDocsListItem(page: AiMarkdownPage): string {
277
312
  return `- [${page.title}](${page.markdownUrl})${description}`;
278
313
  }
279
314
 
315
+ type LlmsTxtSection = {
316
+ title: string;
317
+ pages: AiMarkdownPage[];
318
+ };
319
+
320
+ type LlmsNavPageItem = string | NavPage | NavGroup | NavOpenApiPage;
321
+
322
+ type LlmsNavigationContainer = {
323
+ pages?: LlmsNavPageItem[];
324
+ menu?: NavMenu;
325
+ openapi?: string | NavOpenApi;
326
+ };
327
+
328
+ function getAiMarkdownPageKey(page: AiMarkdownPage): string {
329
+ return page.routeIdentity ?? `${page.filePath}:${page.markdownUrl}`;
330
+ }
331
+
332
+ function buildLlmsSectionTitle(parts: string[]): string {
333
+ const title = parts
334
+ .map((part) => part.trim())
335
+ .filter(Boolean)
336
+ .join(": ");
337
+
338
+ return title || "Docs";
339
+ }
340
+
341
+ function isNavPageItem(item: LlmsNavPageItem): item is string | NavPage {
342
+ return typeof item === "string" || "page" in item;
343
+ }
344
+
345
+ function isNavGroupItem(item: LlmsNavPageItem): item is NavGroup {
346
+ return typeof item !== "string" && "group" in item;
347
+ }
348
+
349
+ function isNavOpenApiPageItem(
350
+ item: LlmsNavPageItem,
351
+ ): item is NavOpenApiPage {
352
+ return typeof item !== "string" && "openapi" in item;
353
+ }
354
+
355
+ function getNavPageRouteIdentity(item: string | NavPage): string {
356
+ return `mdx:${typeof item === "string" ? item : item.page}`;
357
+ }
358
+
359
+ function getNavOpenApiPageRouteIdentity(item: NavOpenApiPage): string | null {
360
+ const parsedEndpoint = parseOpenApiEndpoint(item.openapi.endpoint);
361
+ if (!parsedEndpoint) return null;
362
+
363
+ return `openapi:${item.openapi.source}:${parsedEndpoint.method} ${parsedEndpoint.path}`;
364
+ }
365
+
366
+ function getNavOpenApiSource(openapi: string | NavOpenApi): string {
367
+ return typeof openapi === "string" ? openapi : openapi.source;
368
+ }
369
+
370
+ function getOpenApiEndpointKey(method: string, pathStr: string): string {
371
+ return `${method.toUpperCase()} ${pathStr.toLowerCase()}`;
372
+ }
373
+
374
+ function matchesOpenApiEndpointFilter(
375
+ route: OpenApiRoute,
376
+ endpoints: string[],
377
+ ): boolean {
378
+ const routeEndpointKey = getOpenApiEndpointKey(
379
+ route.openApiMethod,
380
+ route.openApiPath,
381
+ );
382
+
383
+ return endpoints.some((endpoint) => {
384
+ const parsedEndpoint = parseOpenApiEndpoint(endpoint);
385
+ if (!parsedEndpoint) return false;
386
+
387
+ return (
388
+ getOpenApiEndpointKey(parsedEndpoint.method, parsedEndpoint.path) ===
389
+ routeEndpointKey
390
+ );
391
+ });
392
+ }
393
+
394
+ function getOpenApiFileRouteIdentities(
395
+ openapi: string | NavOpenApi,
396
+ routes: Route[],
397
+ ): string[] {
398
+ const source = getNavOpenApiSource(openapi);
399
+ const include = typeof openapi === "string" ? undefined : openapi.include;
400
+ const exclude = typeof openapi === "string" ? undefined : openapi.exclude;
401
+
402
+ return routes
403
+ .filter((route): route is OpenApiRoute => {
404
+ if (route.type !== "openapi" || route.filePath !== source) return false;
405
+ if (include && !matchesOpenApiEndpointFilter(route, include)) {
406
+ return false;
407
+ }
408
+ if (exclude && matchesOpenApiEndpointFilter(route, exclude)) {
409
+ return false;
410
+ }
411
+
412
+ return true;
413
+ })
414
+ .map((route) => route.routeIdentity);
415
+ }
416
+
417
+ function buildLlmsTxtSections(args: {
418
+ config: DocsConfig;
419
+ pages: AiMarkdownPage[];
420
+ routes: Route[];
421
+ }): LlmsTxtSection[] {
422
+ const pageByRouteIdentity = new Map(
423
+ args.pages
424
+ .filter((page) => page.routeIdentity)
425
+ .map((page) => [page.routeIdentity as string, page]),
426
+ );
427
+ const emittedPageKeys = new Set<string>();
428
+ const sections: LlmsTxtSection[] = [];
429
+
430
+ const appendSection = (titleParts: string[], routeIdentities: string[]) => {
431
+ const sectionPages: AiMarkdownPage[] = [];
432
+
433
+ for (const routeIdentity of routeIdentities) {
434
+ const page = pageByRouteIdentity.get(routeIdentity);
435
+ if (!page) continue;
436
+
437
+ const pageKey = getAiMarkdownPageKey(page);
438
+ if (emittedPageKeys.has(pageKey)) continue;
439
+
440
+ emittedPageKeys.add(pageKey);
441
+ sectionPages.push(page);
442
+ }
443
+
444
+ if (sectionPages.length === 0) return;
445
+
446
+ const title = buildLlmsSectionTitle(titleParts);
447
+ const existingSection = sections.find((section) => section.title === title);
448
+
449
+ if (existingSection) {
450
+ existingSection.pages.push(...sectionPages);
451
+ return;
452
+ }
453
+
454
+ sections.push({
455
+ title,
456
+ pages: sectionPages,
457
+ });
458
+ };
459
+
460
+ const collectPages = (items: LlmsNavPageItem[], titleParts: string[]) => {
461
+ let directRouteIdentities: string[] = [];
462
+ const flushDirectPages = () => {
463
+ appendSection(titleParts, directRouteIdentities);
464
+ directRouteIdentities = [];
465
+ };
466
+
467
+ for (const item of items) {
468
+ if (isNavPageItem(item)) {
469
+ directRouteIdentities.push(getNavPageRouteIdentity(item));
470
+ continue;
471
+ }
472
+
473
+ if (isNavOpenApiPageItem(item)) {
474
+ const routeIdentity = getNavOpenApiPageRouteIdentity(item);
475
+ if (routeIdentity) directRouteIdentities.push(routeIdentity);
476
+ continue;
477
+ }
478
+
479
+ if (isNavGroupItem(item)) {
480
+ flushDirectPages();
481
+ collectNavigationContainer(
482
+ { pages: item.pages },
483
+ [...titleParts, item.group],
484
+ );
485
+ }
486
+ }
487
+
488
+ flushDirectPages();
489
+ };
490
+
491
+ const collectNavigationContainer = (
492
+ container: LlmsNavigationContainer,
493
+ titleParts: string[],
494
+ ) => {
495
+ if (container.pages) {
496
+ collectPages(container.pages, titleParts);
497
+ }
498
+
499
+ if (container.openapi) {
500
+ appendSection(
501
+ titleParts.length ? titleParts : ["API Reference"],
502
+ getOpenApiFileRouteIdentities(container.openapi, args.routes),
503
+ );
504
+ }
505
+
506
+ if (container.menu) {
507
+ for (const menuItem of container.menu.items) {
508
+ collectNavigationContainer(menuItem, [...titleParts, menuItem.label]);
509
+ }
510
+ }
511
+ };
512
+
513
+ const navigation = args.config.navigation;
514
+
515
+ if (navigation.pages) {
516
+ collectPages(navigation.pages, []);
517
+ } else if (navigation.menu) {
518
+ collectNavigationContainer({ menu: navigation.menu }, []);
519
+ } else if (navigation.openapi) {
520
+ collectNavigationContainer({ openapi: navigation.openapi }, [
521
+ "API Reference",
522
+ ]);
523
+ } else if (navigation.tabs) {
524
+ for (const tabItem of navigation.tabs.items) {
525
+ collectNavigationContainer(tabItem, [tabItem.label]);
526
+ }
527
+ }
528
+
529
+ const remainingPages = args.pages.filter(
530
+ (page) => !emittedPageKeys.has(getAiMarkdownPageKey(page)),
531
+ );
532
+
533
+ if (remainingPages.length > 0) {
534
+ sections.push({
535
+ title: sections.length ? "Additional Docs" : "Docs",
536
+ pages: remainingPages,
537
+ });
538
+ }
539
+
540
+ return sections;
541
+ }
542
+
280
543
  function normalizeLocalOpenApiSource(source: string): string {
281
544
  return normalizeDocsRootRelativePath(source);
282
545
  }
@@ -297,7 +560,9 @@ function getOpenApiSpecLabel(source: string): string {
297
560
  if (isRemoteUrl(source)) {
298
561
  try {
299
562
  const url = new URL(source);
300
- return path.posix.basename(url.pathname).replace(/\.[^.]+$/, "") || source;
563
+ return (
564
+ path.posix.basename(url.pathname).replace(/\.[^.]+$/, "") || source
565
+ );
301
566
  } catch {
302
567
  return source;
303
568
  }
@@ -344,7 +609,9 @@ function createOpenApiSpecArtifact(source: string): OpenApiSpecArtifact | null {
344
609
  };
345
610
  }
346
611
 
347
- export async function getOpenApiSpecArtifacts(): Promise<OpenApiSpecArtifact[]> {
612
+ export async function getOpenApiSpecArtifacts(): Promise<
613
+ OpenApiSpecArtifact[]
614
+ > {
348
615
  const routes = await getAllRoutes();
349
616
  const sources = new Set(
350
617
  routes
@@ -566,10 +833,7 @@ function getResponseContentTypes(response: any): string[] {
566
833
  return Object.keys(content);
567
834
  }
568
835
 
569
- function appendOpenApiResponsesMarkdown(
570
- lines: string[],
571
- responses: unknown,
572
- ) {
836
+ function appendOpenApiResponsesMarkdown(lines: string[], responses: unknown) {
573
837
  if (!responses || typeof responses !== "object") return;
574
838
 
575
839
  const responseEntries = Object.entries(responses).filter(
@@ -662,6 +926,7 @@ async function createOpenApiMarkdownPage(args: {
662
926
  args.canonicalPath ?? getCanonicalPathForSlug(routeSlug);
663
927
 
664
928
  return {
929
+ routeIdentity: args.route.routeIdentity,
665
930
  filePath: normalizeDocsRootRelativePath(args.route.filePath),
666
931
  routeSlug,
667
932
  title: args.route.title,
@@ -690,6 +955,7 @@ async function createRouteMarkdownPage(
690
955
  return createMarkdownPage({
691
956
  filePath: route.filePath,
692
957
  routeSlug: route.slug,
958
+ routeIdentity: route.routeIdentity,
693
959
  title: route.title,
694
960
  ...overrides,
695
961
  });
@@ -729,16 +995,30 @@ export async function getLlmsTxt(): Promise<string> {
729
995
 
730
996
  const config = await getConfig();
731
997
  const pages = await getLlmsListedMarkdownPages();
998
+ const routes = await getAllRoutes();
732
999
  const specs = await getOpenApiSpecArtifacts();
733
- const lines = [`# ${config.title}`, "", "## Docs", ""];
734
-
735
- lines.push(...pages.map(formatDocsListItem));
1000
+ const sections = buildLlmsTxtSections({ config, pages, routes });
1001
+ const lines = [`# ${config.title}`];
1002
+
1003
+ for (const section of sections) {
1004
+ lines.push(
1005
+ "",
1006
+ `## ${section.title}`,
1007
+ "",
1008
+ ...section.pages.map(formatDocsListItem),
1009
+ );
1010
+ }
736
1011
 
737
1012
  const specsSection = formatOpenApiSpecsSection(specs);
738
1013
  if (specsSection.length > 0) {
739
1014
  lines.push("", ...specsSection);
740
1015
  }
741
1016
 
1017
+ const mcpSection = formatMcpSection();
1018
+ if (mcpSection.length > 0) {
1019
+ lines.push("", ...mcpSection);
1020
+ }
1021
+
742
1022
  return `${lines.join("\n").trimEnd()}\n`;
743
1023
  }
744
1024
 
@@ -785,7 +1065,9 @@ export async function getLlmsFullTxt(): Promise<string> {
785
1065
  continue;
786
1066
  }
787
1067
 
788
- sections.push(formatLlmsFullSpec(spec, await readOpenApiSpecArtifact(spec)));
1068
+ sections.push(
1069
+ formatLlmsFullSpec(spec, await readOpenApiSpecArtifact(spec)),
1070
+ );
789
1071
  }
790
1072
 
791
1073
  return `${sections.join("\n\n---\n\n").trimEnd()}\n`;
@@ -3,7 +3,7 @@ import {
3
3
  getMarkdownRoutePages,
4
4
  MARKDOWN_CONTENT_TYPE,
5
5
  type AiMarkdownPage,
6
- } from "../../lib/ai-artifacts";
6
+ } from "../lib/ai-artifacts";
7
7
 
8
8
  export async function getStaticPaths() {
9
9
  const pages = await getMarkdownRoutePages();