blodemd 0.0.13 → 0.0.14

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 (136) hide show
  1. package/README.md +1 -1
  2. package/dev-server/lib/local-runtime.tsx +12 -15
  3. package/dev-server/package.json +5 -1
  4. package/dist/cli.mjs +1193 -978
  5. package/dist/cli.mjs.map +1 -1
  6. package/docs/app/globals.css +1 -1
  7. package/docs/components/api/api-playground.tsx +2 -5
  8. package/docs/components/api/api-reference.tsx +29 -3
  9. package/docs/components/docs/contextual-menu.tsx +37 -33
  10. package/docs/components/docs/copy-page-menu.tsx +18 -0
  11. package/docs/components/docs/doc-header.tsx +4 -1
  12. package/docs/components/docs/doc-shell.tsx +48 -18
  13. package/docs/components/docs/doc-sidebar.tsx +131 -89
  14. package/docs/components/docs/doc-toc.tsx +1 -0
  15. package/docs/components/docs/mobile-nav.tsx +72 -68
  16. package/docs/components/docs/sidebar-active-highlight.tsx +34 -0
  17. package/docs/components/docs/sidebar-scroll-area.tsx +9 -4
  18. package/docs/components/git/repo-picker.tsx +5 -2
  19. package/docs/components/mdx/index.tsx +31 -21
  20. package/docs/components/posthog-provider.tsx +42 -0
  21. package/docs/components/tenant-analytics.tsx +49 -0
  22. package/docs/components/third-parties.tsx +4 -1
  23. package/docs/components/ui/copy-button.tsx +16 -2
  24. package/docs/components/ui/search-dialog.tsx +431 -0
  25. package/docs/components/ui/search.tsx +44 -427
  26. package/docs/components/web-mcp.tsx +115 -0
  27. package/docs/lib/agent-skills.ts +48 -0
  28. package/docs/lib/config.ts +4 -5
  29. package/docs/lib/docs-collection.ts +7 -22
  30. package/docs/lib/docs-runtime.tsx +160 -10
  31. package/docs/lib/llms-full.ts +82 -0
  32. package/docs/lib/marketing-markdown.ts +79 -0
  33. package/docs/lib/marketing-site.ts +31 -0
  34. package/docs/lib/mdx.ts +17 -6
  35. package/docs/lib/routes.ts +19 -3
  36. package/docs/lib/shiki.ts +4 -0
  37. package/docs/lib/tenancy.ts +15 -2
  38. package/docs/lib/tenant-headers.ts +29 -0
  39. package/docs/lib/tenant-static.ts +73 -99
  40. package/package.json +8 -5
  41. package/packages/@repo/common/package.json +1 -1
  42. package/packages/@repo/contracts/dist/analytics.d.ts +21 -0
  43. package/packages/@repo/contracts/dist/analytics.d.ts.map +1 -0
  44. package/packages/@repo/contracts/dist/analytics.js +18 -0
  45. package/packages/@repo/contracts/dist/deployment.js +1 -1
  46. package/packages/@repo/contracts/dist/git.js +2 -2
  47. package/packages/@repo/contracts/dist/ids.d.ts.map +1 -1
  48. package/packages/@repo/contracts/dist/ids.js +2 -1
  49. package/packages/@repo/contracts/dist/index.d.ts +1 -0
  50. package/packages/@repo/contracts/dist/index.d.ts.map +1 -1
  51. package/packages/@repo/contracts/dist/index.js +1 -0
  52. package/packages/@repo/contracts/dist/project.d.ts +18 -1
  53. package/packages/@repo/contracts/dist/project.d.ts.map +1 -1
  54. package/packages/@repo/contracts/dist/project.js +3 -1
  55. package/packages/@repo/contracts/dist/tenant.d.ts +36 -0
  56. package/packages/@repo/contracts/dist/tenant.d.ts.map +1 -1
  57. package/packages/@repo/contracts/dist/tenant.js +2 -0
  58. package/packages/@repo/contracts/package.json +1 -1
  59. package/packages/@repo/contracts/src/analytics.ts +33 -0
  60. package/packages/@repo/contracts/src/deployment.ts +1 -1
  61. package/packages/@repo/contracts/src/git.ts +2 -2
  62. package/packages/@repo/contracts/src/ids.ts +2 -1
  63. package/packages/@repo/contracts/src/index.ts +1 -0
  64. package/packages/@repo/contracts/src/project.ts +3 -1
  65. package/packages/@repo/contracts/src/tenant.ts +2 -0
  66. package/packages/@repo/models/dist/docs-config.d.ts +0 -198
  67. package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
  68. package/packages/@repo/models/dist/docs-config.js +3 -2
  69. package/packages/@repo/models/dist/tenant.d.ts +12 -0
  70. package/packages/@repo/models/dist/tenant.d.ts.map +1 -1
  71. package/packages/@repo/models/package.json +1 -1
  72. package/packages/@repo/models/src/docs-config.ts +3 -3
  73. package/packages/@repo/models/src/tenant.ts +15 -0
  74. package/packages/@repo/prebuild/dist/openapi.d.ts.map +1 -1
  75. package/packages/@repo/prebuild/dist/openapi.js +32 -4
  76. package/packages/@repo/prebuild/package.json +1 -1
  77. package/packages/@repo/prebuild/src/openapi.ts +42 -4
  78. package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
  79. package/packages/@repo/previewing/dist/blob-source.js +18 -5
  80. package/packages/@repo/previewing/dist/config-loader.d.ts +134 -0
  81. package/packages/@repo/previewing/dist/config-loader.d.ts.map +1 -0
  82. package/packages/@repo/previewing/dist/config-loader.js +231 -0
  83. package/packages/@repo/previewing/dist/constants.d.ts +15 -0
  84. package/packages/@repo/previewing/dist/constants.d.ts.map +1 -0
  85. package/packages/@repo/previewing/dist/constants.js +14 -0
  86. package/packages/@repo/previewing/dist/content-index.d.ts +7 -0
  87. package/packages/@repo/previewing/dist/content-index.d.ts.map +1 -0
  88. package/packages/@repo/previewing/dist/content-index.js +291 -0
  89. package/packages/@repo/previewing/dist/index.d.ts +13 -121
  90. package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
  91. package/packages/@repo/previewing/dist/index.js +11 -1003
  92. package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts +3 -0
  93. package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts.map +1 -0
  94. package/packages/@repo/previewing/dist/markdown/agent-markdown.js +176 -0
  95. package/packages/@repo/previewing/dist/markdown/format.d.ts +8 -0
  96. package/packages/@repo/previewing/dist/markdown/format.d.ts.map +1 -0
  97. package/packages/@repo/previewing/dist/markdown/format.js +43 -0
  98. package/packages/@repo/previewing/dist/markdown/links.d.ts +3 -0
  99. package/packages/@repo/previewing/dist/markdown/links.d.ts.map +1 -0
  100. package/packages/@repo/previewing/dist/markdown/links.js +31 -0
  101. package/packages/@repo/previewing/dist/openapi-pages.d.ts +7 -0
  102. package/packages/@repo/previewing/dist/openapi-pages.d.ts.map +1 -0
  103. package/packages/@repo/previewing/dist/openapi-pages.js +200 -0
  104. package/packages/@repo/previewing/dist/search-index.d.ts +4 -0
  105. package/packages/@repo/previewing/dist/search-index.d.ts.map +1 -0
  106. package/packages/@repo/previewing/dist/search-index.js +23 -0
  107. package/packages/@repo/previewing/dist/serialization.d.ts +13 -0
  108. package/packages/@repo/previewing/dist/serialization.d.ts.map +1 -0
  109. package/packages/@repo/previewing/dist/serialization.js +108 -0
  110. package/packages/@repo/previewing/dist/toc-index.d.ts +5 -0
  111. package/packages/@repo/previewing/dist/toc-index.d.ts.map +1 -0
  112. package/packages/@repo/previewing/dist/toc-index.js +53 -0
  113. package/packages/@repo/previewing/dist/types.d.ts +92 -0
  114. package/packages/@repo/previewing/dist/types.d.ts.map +1 -0
  115. package/packages/@repo/previewing/dist/types.js +1 -0
  116. package/packages/@repo/previewing/dist/utility-index.d.ts +7 -0
  117. package/packages/@repo/previewing/dist/utility-index.d.ts.map +1 -0
  118. package/packages/@repo/previewing/dist/utility-index.js +201 -0
  119. package/packages/@repo/previewing/package.json +1 -1
  120. package/packages/@repo/previewing/src/blob-source.ts +20 -5
  121. package/packages/@repo/previewing/src/config-loader.ts +316 -0
  122. package/packages/@repo/previewing/src/constants.ts +16 -0
  123. package/packages/@repo/previewing/src/content-index.ts +384 -0
  124. package/packages/@repo/previewing/src/index.ts +69 -1515
  125. package/packages/@repo/previewing/src/markdown/agent-markdown.ts +254 -0
  126. package/packages/@repo/previewing/src/markdown/format.ts +63 -0
  127. package/packages/@repo/previewing/src/markdown/links.ts +46 -0
  128. package/packages/@repo/previewing/src/openapi-pages.ts +319 -0
  129. package/packages/@repo/previewing/src/search-index.ts +38 -0
  130. package/packages/@repo/previewing/src/serialization.ts +191 -0
  131. package/packages/@repo/previewing/src/toc-index.ts +72 -0
  132. package/packages/@repo/previewing/src/types.ts +105 -0
  133. package/packages/@repo/previewing/src/utility-index.ts +304 -0
  134. package/packages/@repo/validation/package.json +1 -1
  135. package/scripts/repo-packages.mjs +8 -0
  136. package/docs/components/ui/site-footer.tsx +0 -41
@@ -1,1517 +1,71 @@
1
- import path from "node:path";
2
-
3
- import { ensureArray, normalizePath, slugify } from "@repo/common";
4
- import type {
5
- CollectionConfig,
6
- ContentType,
7
- DocsConfig,
8
- DocsOpenApiSource,
9
- FrontmatterByType,
10
- PageMode,
11
- SiteConfig,
12
- } from "@repo/models";
13
- import { PageModeSchema } from "@repo/models";
14
- import {
15
- extractOpenApiOperations,
16
- openApiIdentifier,
17
- openApiSlug,
18
- parseOpenApiSpec,
19
- } from "@repo/prebuild";
20
- import type { OpenApiOperation, OpenApiSpec } from "@repo/prebuild";
21
- import {
22
- validateDocsConfig,
23
- validateFrontmatter,
24
- validateSiteConfig,
25
- } from "@repo/validation";
26
- import YAML from "yaml";
27
-
28
- import type { ContentSource } from "./content-source.js";
29
-
30
1
  export { BlobContentSource, createBlobSource } from "./blob-source.js";
31
- export { createFsSource, FsContentSource } from "./fs-source.js";
2
+ export {
3
+ getDocsCollection,
4
+ getDocsCollectionWithNavigation,
5
+ getDocsNavigation,
6
+ loadSiteConfig,
7
+ } from "./config-loader.js";
8
+ export {
9
+ LEGACY_PROJECT_NAME_FALLBACK_WARNING,
10
+ PREBUILT_INDEX_PATH,
11
+ PREBUILT_OPENAPI_INDEX_PATH,
12
+ PREBUILT_SEARCH_INDEX_PATH,
13
+ PREBUILT_TOC_INDEX_PATH,
14
+ PREBUILT_UTILITY_INDEX_PATH,
15
+ PREBUILT_UTILITY_LLMS_FULL_PATH,
16
+ PREBUILT_UTILITY_LLMS_PATH,
17
+ PREBUILT_UTILITY_LLMS_SEGMENT_PREFIX,
18
+ PREBUILT_UTILITY_SITEMAP_PATH,
19
+ PREBUILT_UTILITY_SKILLS_INDEX_PATH,
20
+ PREBUILT_UTILITY_SKILLS_MD_PREFIX,
21
+ UTILITY_DOCS_ROOT_TOKEN,
22
+ } from "./constants.js";
23
+ export { buildContentIndex, buildPageMetadataMap } from "./content-index.js";
32
24
  export type { CompiledMdxResult, ContentSource } from "./content-source.js";
33
-
34
- export const PREBUILT_INDEX_PATH = "_content-index.json";
35
- export const PREBUILT_OPENAPI_INDEX_PATH = "_openapi-index.json";
36
- export const PREBUILT_SEARCH_INDEX_PATH = "_search-index.json";
37
- export const PREBUILT_TOC_INDEX_PATH = "_toc-index.json";
38
- export const PREBUILT_UTILITY_INDEX_PATH = "_utility-index.json";
39
- export const PREBUILT_UTILITY_SITEMAP_PATH = "_utility/sitemap.xml";
40
- export const PREBUILT_UTILITY_LLMS_PATH = "_utility/llms.txt";
41
- export const PREBUILT_UTILITY_LLMS_FULL_PATH = "_utility/llms-full.txt";
42
- export const PREBUILT_UTILITY_SKILLS_INDEX_PATH = "_utility/skills-index.json";
43
- export const PREBUILT_UTILITY_SKILLS_MD_PREFIX = "_utility/skills/";
44
- export const UTILITY_DOCS_ROOT_TOKEN = "__BLODEMD_DOCS_ROOT__";
45
- export const LEGACY_PROJECT_NAME_FALLBACK_WARNING =
46
- "docs.json.slug is recommended. Falling back to docs.json.name as the deployment slug is deprecated.";
47
-
48
- export type SiteConfigResult =
49
- | { ok: true; config: SiteConfig; warnings: string[] }
50
- | { ok: false; errors: string[] };
51
-
52
- export type ContentEntry =
53
- | {
54
- kind: "entry";
55
- slug: string;
56
- title: string;
57
- description?: string;
58
- hidden?: boolean;
59
- type: ContentType;
60
- collectionId: string;
61
- sourcePath: string;
62
- relativePath: string;
63
- frontmatter: FrontmatterByType[ContentType];
64
- }
65
- | {
66
- kind: "index";
67
- slug: string;
68
- title: string;
69
- description?: string;
70
- type: ContentType;
71
- collectionId: string;
72
- };
73
-
74
- export interface ContentIndex {
75
- entries: ContentEntry[];
76
- bySlug: Map<string, ContentEntry>;
77
- byCollection: Map<string, ContentEntry[]>;
78
- errors: string[];
79
- }
80
-
81
- export interface PageMetadata {
82
- title?: string;
83
- sidebarTitle?: string;
84
- icon?: string;
85
- iconType?: string;
86
- tag?: string;
87
- hidden?: boolean;
88
- deprecated?: boolean;
89
- url?: string;
90
- mode?: PageMode;
91
- noindex?: boolean;
92
- hideFooterPagination?: boolean;
93
- hideApiMarker?: boolean;
94
- keywords?: string[];
95
- }
96
-
97
- export interface SearchIndexItem {
98
- href?: string;
99
- title: string;
100
- path: string;
101
- }
102
-
103
- export interface TocItem {
104
- id: string;
105
- level: number;
106
- title: string;
107
- }
108
-
109
- export interface UtilityPage {
110
- content: string;
111
- description?: string;
112
- slug: string;
113
- title: string;
114
- }
115
-
116
- export interface UtilityIndex {
117
- description?: string;
118
- name: string;
119
- pages: UtilityPage[];
120
- slug?: string;
121
- }
122
-
123
- export interface UtilityArtifact {
124
- content: string;
125
- contentType: string;
126
- path: string;
127
- }
128
-
129
- export interface PrebuiltOpenApiEntry {
130
- identifier: string;
131
- operation: OpenApiOperation;
132
- slug: string;
133
- source: DocsOpenApiSource;
134
- sourceKey: string;
135
- spec: OpenApiSpec;
136
- }
137
-
138
- const validModes = new Set<string>(PageModeSchema.options);
139
-
140
- export const buildPageMetadataMap = (
141
- index: ContentIndex
142
- ): Map<string, PageMetadata> => {
143
- const map = new Map<string, PageMetadata>();
144
- for (const entry of index.entries) {
145
- if (entry.kind !== "entry") {
146
- continue;
147
- }
148
- const fm = entry.frontmatter as Record<string, unknown>;
149
- const meta: PageMetadata = {
150
- title: entry.title,
151
- };
152
- const hasFields = true;
153
- if (typeof fm.sidebarTitle === "string") {
154
- meta.sidebarTitle = fm.sidebarTitle;
155
- }
156
- if (typeof fm.icon === "string") {
157
- meta.icon = fm.icon;
158
- }
159
- if (typeof fm.iconType === "string") {
160
- meta.iconType = fm.iconType;
161
- }
162
- if (typeof fm.tag === "string") {
163
- meta.tag = fm.tag;
164
- }
165
- if (typeof fm.hidden === "boolean") {
166
- meta.hidden = fm.hidden;
167
- }
168
- if (typeof fm.deprecated === "boolean") {
169
- meta.deprecated = fm.deprecated;
170
- }
171
- if (typeof fm.url === "string") {
172
- meta.url = fm.url;
173
- }
174
- if (typeof fm.mode === "string" && validModes.has(fm.mode)) {
175
- meta.mode = fm.mode as PageMode;
176
- }
177
- if (typeof fm.noindex === "boolean") {
178
- meta.noindex = fm.noindex;
179
- }
180
- if (typeof fm.hideFooterPagination === "boolean") {
181
- meta.hideFooterPagination = fm.hideFooterPagination;
182
- }
183
- if (typeof fm.hideApiMarker === "boolean") {
184
- meta.hideApiMarker = fm.hideApiMarker;
185
- }
186
- if (Array.isArray(fm.keywords)) {
187
- meta.keywords = fm.keywords as string[];
188
- }
189
- if (hasFields) {
190
- map.set(entry.slug, meta);
191
- }
192
- }
193
- return map;
194
- };
195
-
196
- const DOCS_CONFIG_FILE = "docs.json";
197
- const DOC_FILE_EXTENSION_REGEX = /\.(mdx|md)$/;
198
- const FRONTMATTER_REGEX = /^---\s*\n([\s\S]*?)\n---\s*\n?/;
199
- const INDEX_SUFFIX = "/index";
200
-
201
- const titleFromSlug = (slug: string) => {
202
- const clean = slug.replaceAll("-", " ").split("/").pop() ?? slug;
203
- if (clean === "index") {
204
- return "Overview";
205
- }
206
- return clean.replaceAll(/\b\w/g, (char) => char.toUpperCase());
207
- };
208
-
209
- const parseFrontmatter = (source: string) => {
210
- const match = FRONTMATTER_REGEX.exec(source);
211
- if (!match) {
212
- return { body: source, frontmatter: {} };
213
- }
214
- const raw = match[1] ?? "";
215
- const data = YAML.parse(raw) ?? {};
216
- const body = source.slice(match[0].length);
217
- return { body, frontmatter: data };
218
- };
219
-
220
- const slugFromFile = (relativePath: string) => {
221
- const clean = normalizePath(relativePath);
222
- const withoutExt = clean.replace(DOC_FILE_EXTENSION_REGEX, "");
223
- if (withoutExt.endsWith(INDEX_SUFFIX)) {
224
- const trimmed = withoutExt.slice(0, -INDEX_SUFFIX.length);
225
- return trimmed.length ? trimmed : "index";
226
- }
227
- return withoutExt.length ? withoutExt : "index";
228
- };
229
-
230
- const defaultLinkLabel = (input: {
231
- href: string;
232
- label?: string;
233
- type?: "discord" | "github";
234
- }) => {
235
- if (input.label) {
236
- return input.label;
237
- }
238
- if (input.type === "github") {
239
- return "GitHub";
240
- }
241
- if (input.type === "discord") {
242
- return "Discord";
243
- }
244
- try {
245
- return new URL(input.href).hostname;
246
- } catch {
247
- return input.href;
248
- }
249
- };
250
-
251
- // oxlint-disable-next-line eslint/complexity
252
- const mapDocsConfig = (docs: DocsConfig): SiteConfig => {
253
- const navigation = {
254
- global:
255
- docs.navbar?.links?.length || docs.navigation.global?.anchors?.length
256
- ? {
257
- anchors: docs.navigation.global?.anchors?.map((anchor) => ({
258
- href: anchor.href,
259
- label: anchor.anchor,
260
- })),
261
- links: docs.navbar?.links?.map((link) => ({
262
- href: link.href,
263
- label: defaultLinkLabel(link),
264
- })),
265
- }
266
- : undefined,
267
- groups: docs.navigation.groups?.map((group) => ({
268
- expanded: group.expanded,
269
- group: group.group,
270
- hidden: group.hidden,
271
- pages: group.root
272
- ? [
273
- group.root,
274
- ...(group.pages ?? []).filter((page) => page !== group.root),
275
- ]
276
- : group.pages,
277
- })),
278
- languages: docs.navigation.languages?.map((language) => ({
279
- label: language.language,
280
- locale: language.language,
281
- url: language.href,
282
- })),
283
- pages: docs.navigation.pages,
284
- tabs: docs.navigation.tabs?.map((tab) => ({
285
- groups: tab.groups?.map((group) => ({
286
- expanded: group.expanded,
287
- group: group.group,
288
- hidden: group.hidden,
289
- pages: group.root
290
- ? [
291
- group.root,
292
- ...(group.pages ?? []).filter((page) => page !== group.root),
293
- ]
294
- : group.pages,
295
- })),
296
- href: tab.href,
297
- icon: tab.icon,
298
- label: tab.tab,
299
- pages: tab.pages,
300
- })),
301
- versions: docs.navigation.versions?.map((version) => ({
302
- label: version.version,
303
- url: version.href,
304
- })),
305
- } satisfies SiteConfig["navigation"];
306
-
307
- return {
308
- collections: [
309
- {
310
- id: "docs",
311
- navigation,
312
- openapi: docs.api?.openapi,
313
- root: "",
314
- type: "docs",
315
- },
316
- ],
317
- contextual: docs.contextual,
318
- description: docs.description,
319
- favicon:
320
- typeof docs.favicon === "string" ? docs.favicon : docs.favicon?.light,
321
- features: {
322
- rightToc: true,
323
- search: true,
324
- themeToggle: docs.appearance?.strict !== true,
325
- toc: true,
326
- },
327
- logo: docs.logo
328
- ? {
329
- dark: typeof docs.logo === "string" ? docs.logo : docs.logo.dark,
330
- href: typeof docs.logo === "string" ? undefined : docs.logo.href,
331
- light: typeof docs.logo === "string" ? docs.logo : docs.logo.light,
332
- }
333
- : undefined,
334
- name: docs.name,
335
- navigation,
336
- openapiProxy: {
337
- enabled:
338
- docs.api?.playground?.proxy !== false &&
339
- Boolean(docs.api?.openapi || docs.api?.asyncapi),
340
- },
341
- seo: docs.seo,
342
- slug: docs.slug,
343
- };
344
- };
345
-
346
- const getProjectWarnings = (config: { slug?: string }): string[] =>
347
- config.slug ? [] : [LEGACY_PROJECT_NAME_FALLBACK_WARNING];
348
-
349
- const readJsonConfig = async (source: ContentSource, relativePath: string) =>
350
- JSON.parse(await source.readFile(relativePath)) as unknown;
351
-
352
- const normalizeRefPath = (baseDirectory: string, reference: string) => {
353
- if (
354
- reference.startsWith("/") ||
355
- reference.startsWith("\\") ||
356
- reference.startsWith("http://") ||
357
- reference.startsWith("https://")
358
- ) {
359
- throw new Error(
360
- `Invalid $ref "${reference}". Only relative JSON files are supported.`
361
- );
362
- }
363
-
364
- const normalized = normalizePath(path.posix.join(baseDirectory, reference));
365
- if (
366
- !normalized ||
367
- normalized === "." ||
368
- normalized.startsWith("../") ||
369
- normalized.includes("/../")
370
- ) {
371
- throw new Error(`Invalid $ref "${reference}".`);
372
- }
373
- return normalized;
374
- };
375
-
376
- const resolveJsonRefs = async (
377
- source: ContentSource,
378
- value: unknown,
379
- baseDirectory: string,
380
- seen: Set<string>
381
- ): Promise<unknown> => {
382
- if (Array.isArray(value)) {
383
- return await Promise.all(
384
- value.map((item) => resolveJsonRefs(source, item, baseDirectory, seen))
385
- );
386
- }
387
-
388
- if (!value || typeof value !== "object") {
389
- return value;
390
- }
391
-
392
- const record = value as Record<string, unknown>;
393
- const reference = record.$ref;
394
- if (typeof reference === "string") {
395
- const resolvedPath = normalizeRefPath(baseDirectory, reference);
396
- if (seen.has(resolvedPath)) {
397
- throw new Error(`Circular $ref detected for "${resolvedPath}".`);
398
- }
399
-
400
- const nextSeen = new Set(seen);
401
- // oxlint-disable-next-line eslint-plugin-unicorn/no-immediate-mutation
402
- nextSeen.add(resolvedPath);
403
- const referenced = await readJsonConfig(source, resolvedPath);
404
- const referencedValue = await resolveJsonRefs(
405
- source,
406
- referenced,
407
- path.posix.dirname(resolvedPath) === "."
408
- ? ""
409
- : normalizePath(path.posix.dirname(resolvedPath)),
410
- nextSeen
411
- );
412
-
413
- const siblingEntries = Object.entries(record).filter(
414
- ([key]) => key !== "$ref"
415
- );
416
- if (
417
- !siblingEntries.length ||
418
- !referencedValue ||
419
- typeof referencedValue !== "object" ||
420
- Array.isArray(referencedValue)
421
- ) {
422
- return referencedValue;
423
- }
424
-
425
- const siblingValue = await resolveJsonRefs(
426
- source,
427
- Object.fromEntries(siblingEntries),
428
- baseDirectory,
429
- seen
430
- );
431
- return {
432
- ...(referencedValue as Record<string, unknown>),
433
- ...(siblingValue as Record<string, unknown>),
434
- };
435
- }
436
-
437
- const resolvedEntries = await Promise.all(
438
- Object.entries(record).map(async ([key, entryValue]) => [
439
- key,
440
- await resolveJsonRefs(source, entryValue, baseDirectory, seen),
441
- ])
442
- );
443
- return Object.fromEntries(resolvedEntries);
444
- };
445
-
446
- const readResolvedJsonConfig = async (
447
- source: ContentSource,
448
- relativePath: string
449
- ) =>
450
- await resolveJsonRefs(
451
- source,
452
- await readJsonConfig(source, relativePath),
453
- path.posix.dirname(relativePath) === "."
454
- ? ""
455
- : normalizePath(path.posix.dirname(relativePath)),
456
- new Set([relativePath])
457
- );
458
-
459
- const loadDocsConfig = async (
460
- source: ContentSource
461
- ): Promise<SiteConfigResult | null> => {
462
- if (!(await source.exists(DOCS_CONFIG_FILE))) {
463
- return null;
464
- }
465
-
466
- try {
467
- const parsed = await readResolvedJsonConfig(source, DOCS_CONFIG_FILE);
468
-
469
- // Try SiteConfig format first (has collections, theme, colors, etc.)
470
- const siteResult = validateSiteConfig(parsed);
471
- if (siteResult.success) {
472
- return {
473
- config: siteResult.data,
474
- ok: true,
475
- warnings: getProjectWarnings(siteResult.data),
476
- };
477
- }
478
-
479
- // Fall back to DocsConfig format (Mintlify-compatible) and map to SiteConfig
480
- const docsResult = validateDocsConfig(parsed);
481
- if (docsResult.success) {
482
- return {
483
- config: mapDocsConfig(docsResult.data),
484
- ok: true,
485
- warnings: getProjectWarnings(docsResult.data),
486
- };
487
- }
488
-
489
- return { errors: docsResult.errors, ok: false };
490
- } catch (error) {
491
- return {
492
- errors: [
493
- error instanceof Error
494
- ? error.message
495
- : `Failed to load ${DOCS_CONFIG_FILE}`,
496
- ],
497
- ok: false,
498
- };
499
- }
500
- };
501
-
502
- export const loadSiteConfig = async (
503
- source: ContentSource
504
- ): Promise<SiteConfigResult> => {
505
- const docsConfig = await loadDocsConfig(source);
506
- if (docsConfig) {
507
- return docsConfig;
508
- }
509
-
510
- return {
511
- errors: [`${DOCS_CONFIG_FILE} not found.`],
512
- ok: false,
513
- };
514
- };
515
-
516
- export const loadContentSource = async (
517
- source: ContentSource,
518
- relativePath: string
519
- ) => await source.readFile(relativePath);
520
-
521
- const listContentFiles = async (source: ContentSource, directory: string) => {
522
- const files = await source.listFiles(directory);
523
- return files.filter((file: string) => DOC_FILE_EXTENSION_REGEX.test(file));
524
- };
525
-
526
- const sortDefaults: Record<
527
- ContentType,
528
- { field: "date" | "order" | "title" | "price"; direction: "asc" | "desc" }
529
- > = {
530
- blog: { direction: "desc", field: "date" },
531
- courses: { direction: "asc", field: "order" },
532
- docs: { direction: "asc", field: "title" },
533
- forms: { direction: "asc", field: "title" },
534
- notes: { direction: "desc", field: "date" },
535
- products: { direction: "asc", field: "title" },
536
- sheets: { direction: "asc", field: "title" },
537
- site: { direction: "asc", field: "title" },
538
- slides: { direction: "asc", field: "title" },
539
- todos: { direction: "desc", field: "date" },
540
- };
541
-
542
- const normalizeSortValue = (value: unknown) => {
543
- if (typeof value === "number") {
544
- return value;
545
- }
546
- if (typeof value === "string") {
547
- const timestamp = Date.parse(value);
548
- if (!Number.isNaN(timestamp)) {
549
- return timestamp;
550
- }
551
- return value.toLowerCase();
552
- }
553
- return null;
554
- };
555
-
556
- const compareValues = (a: unknown, b: unknown, direction: "asc" | "desc") => {
557
- const left = normalizeSortValue(a);
558
- const right = normalizeSortValue(b);
559
- if (left === null && right === null) {
560
- return 0;
561
- }
562
- if (left === null) {
563
- return 1;
564
- }
565
- if (right === null) {
566
- return -1;
567
- }
568
- const multiplier = direction === "desc" ? -1 : 1;
569
- if (typeof left === "number" && typeof right === "number") {
570
- return (left - right) * multiplier;
571
- }
572
- if (left < right) {
573
- return -1 * multiplier;
574
- }
575
- if (left > right) {
576
- return 1 * multiplier;
577
- }
578
- return 0;
579
- };
580
-
581
- const autoIndexTypes = new Set<ContentType>([
582
- "blog",
583
- "courses",
584
- "products",
585
- "notes",
586
- "forms",
587
- "sheets",
588
- "slides",
589
- "todos",
590
- ]);
591
-
592
- const getCollectionIndex = (
593
- collection: CollectionConfig,
594
- slugPrefix: string
595
- ) => {
596
- if (collection.index) {
597
- return collection.index;
598
- }
599
- if (autoIndexTypes.has(collection.type)) {
600
- const slug = slugPrefix || collection.id;
601
- return {
602
- slug,
603
- title: titleFromSlug(slug),
604
- };
605
- }
606
- return null;
607
- };
608
-
609
- const addEntry = (
610
- entry: ContentEntry,
611
- index: ContentIndex,
612
- errors: string[]
613
- ) => {
614
- if (index.bySlug.has(entry.slug)) {
615
- errors.push(`slug "${entry.slug}" is defined more than once`);
616
- return;
617
- }
618
- index.entries.push(entry);
619
- index.bySlug.set(entry.slug, entry);
620
- };
621
-
622
- const resolveEntrySlug = (relativeSlug: string, slugPrefix: string) => {
623
- if (!slugPrefix) {
624
- return relativeSlug;
625
- }
626
-
627
- if (relativeSlug === "index") {
628
- return slugPrefix;
629
- }
630
-
631
- return normalizePath(`${slugPrefix}/${relativeSlug}`);
632
- };
633
-
634
- const buildEntryFromFile = async ({
635
- collection,
636
- errors,
637
- file,
638
- root,
639
- slugPrefix,
640
- source,
641
- }: {
642
- collection: CollectionConfig;
643
- errors: string[];
644
- file: string;
645
- root: string;
646
- slugPrefix: string;
647
- source: ContentSource;
648
- }): Promise<Extract<ContentEntry, { kind: "entry" }> | null> => {
649
- const sourcePath = root
650
- ? normalizePath(path.join(root, file))
651
- : normalizePath(file);
652
-
653
- let entrySource = "";
654
- try {
655
- entrySource = await source.readFile(sourcePath);
656
- } catch (error) {
657
- errors.push(
658
- error instanceof Error ? error.message : `Failed to read ${sourcePath}`
659
- );
660
- return null;
661
- }
662
-
663
- const { frontmatter } = parseFrontmatter(entrySource);
664
- const frontmatterResult = validateFrontmatter(collection.type, frontmatter);
665
- if (!frontmatterResult.success) {
666
- for (const issue of frontmatterResult.errors) {
667
- errors.push(`${sourcePath}: ${issue}`);
668
- }
669
- }
670
-
671
- const resolvedFrontmatter = frontmatterResult.success
672
- ? frontmatterResult.data
673
- : (frontmatter as FrontmatterByType[ContentType]);
674
- const relativeSlug = slugFromFile(file);
675
- const slug = resolveEntrySlug(relativeSlug, slugPrefix);
676
- const title =
677
- typeof resolvedFrontmatter?.title === "string"
678
- ? resolvedFrontmatter.title
679
- : titleFromSlug(slug);
680
- const description =
681
- typeof resolvedFrontmatter?.description === "string"
682
- ? resolvedFrontmatter.description
683
- : undefined;
684
-
685
- const hidden =
686
- typeof resolvedFrontmatter?.hidden === "boolean"
687
- ? resolvedFrontmatter.hidden
688
- : undefined;
689
-
690
- return {
691
- collectionId: collection.id,
692
- description,
693
- frontmatter: resolvedFrontmatter,
694
- hidden: hidden || undefined,
695
- kind: "entry",
696
- relativePath: sourcePath,
697
- slug,
698
- sourcePath,
699
- title,
700
- type: collection.type,
701
- };
702
- };
703
-
704
- export const buildContentIndex = async (
705
- source: ContentSource,
706
- config: SiteConfig
707
- ): Promise<ContentIndex> => {
708
- const errors: string[] = [];
709
- const index: ContentIndex = {
710
- byCollection: new Map<string, ContentEntry[]>(),
711
- bySlug: new Map<string, ContentEntry>(),
712
- entries: [],
713
- errors,
714
- };
715
-
716
- for (const collection of config.collections) {
717
- const root = normalizePath(collection.root ?? "");
718
- const slugPrefix = normalizePath(collection.slugPrefix ?? "");
719
- let files: string[] = [];
720
- try {
721
- files = await listContentFiles(source, root);
722
- } catch (error) {
723
- errors.push(
724
- error instanceof Error ? error.message : `Failed to read ${root || "."}`
725
- );
726
- continue;
727
- }
728
-
729
- const collectionEntries: ContentEntry[] = [];
730
-
731
- const resolvedEntries = await Promise.all(
732
- files.map(
733
- async (file) =>
734
- await buildEntryFromFile({
735
- collection,
736
- errors,
737
- file,
738
- root,
739
- slugPrefix,
740
- source,
741
- })
742
- )
743
- );
744
-
745
- for (const entry of resolvedEntries) {
746
- if (!entry) {
747
- continue;
748
- }
749
-
750
- collectionEntries.push(entry);
751
- addEntry(entry, index, errors);
752
- }
753
-
754
- const sortConfig = {
755
- ...sortDefaults[collection.type],
756
- ...collection.sort,
757
- };
758
- const sortField = sortConfig.field ?? "title";
759
- const sortDirection = sortConfig.direction ?? "asc";
760
- collectionEntries.sort((left, right) => {
761
- const leftValue =
762
- left.kind === "entry"
763
- ? (left.frontmatter as Record<string, unknown>)[sortField]
764
- : undefined;
765
- const rightValue =
766
- right.kind === "entry"
767
- ? (right.frontmatter as Record<string, unknown>)[sortField]
768
- : undefined;
769
- return compareValues(leftValue, rightValue, sortDirection);
770
- });
771
-
772
- index.byCollection.set(collection.id, collectionEntries);
773
-
774
- const collectionIndex = getCollectionIndex(collection, slugPrefix);
775
- if (collectionIndex) {
776
- const indexEntry: ContentEntry = {
777
- collectionId: collection.id,
778
- description: collectionIndex.description,
779
- kind: "index",
780
- slug: collectionIndex.slug,
781
- title: collectionIndex.title ?? titleFromSlug(collectionIndex.slug),
782
- type: collection.type,
783
- };
784
- addEntry(indexEntry, index, errors);
785
- }
786
- }
787
-
788
- return index;
789
- };
790
-
791
- interface SerializedContentIndex {
792
- version: 1;
793
- entries: ContentEntry[];
794
- collections: Record<string, ContentEntry[]>;
795
- }
796
-
797
- interface SerializedOpenApiIndex {
798
- entries: PrebuiltOpenApiEntry[];
799
- version: 1;
800
- }
801
-
802
- interface SerializedSearchIndex {
803
- items: SearchIndexItem[];
804
- version: 1;
805
- }
806
-
807
- interface SerializedTocIndex {
808
- itemsBySlug: Record<string, TocItem[]>;
809
- version: 1;
810
- }
811
-
812
- interface SerializedUtilityIndex {
813
- description?: string;
814
- name: string;
815
- pages: UtilityPage[];
816
- version: 1;
817
- }
818
-
819
- interface UtilityOpenApiPage extends UtilityPage {
820
- identifier: string;
821
- sourceKey: string;
822
- }
823
-
824
- const NEWLINE_REGEX = /\r?\n/;
825
- const HEADING_REGEX = /^(#{2,4})\s+(.*)$/;
826
- const LEADING_H1_REGEX = /^#\s+([^\r\n]+)(?:\r?\n(?:\r?\n)?)?/;
827
-
828
- export const extractToc = (source: string): TocItem[] => {
829
- const withoutCode = source.replaceAll(/```[\s\S]*?```/g, "");
830
- const lines = withoutCode.split(NEWLINE_REGEX);
831
- const toc: TocItem[] = [];
832
-
833
- for (const line of lines) {
834
- const match = HEADING_REGEX.exec(line.trim());
835
- if (!match) {
836
- continue;
837
- }
838
-
839
- const [, hashes = "", heading = ""] = match;
840
- if (!(hashes && heading)) {
841
- continue;
842
- }
843
-
844
- toc.push({
845
- id: slugify(heading.trim()),
846
- level: hashes.length,
847
- title: heading.trim(),
848
- });
849
- }
850
-
851
- return toc;
852
- };
853
-
854
- const stripMatchingLeadingH1 = (source: string, title: string) => {
855
- const trimmed = source.trimStart();
856
- const match = LEADING_H1_REGEX.exec(trimmed);
857
- if (!match) {
858
- return trimmed.trim();
859
- }
860
-
861
- const [headingLine = "", headingTitle = ""] = match;
862
- if (slugify(headingTitle) !== slugify(title)) {
863
- return trimmed.trim();
864
- }
865
-
866
- return trimmed.slice(headingLine.length).trim();
867
- };
868
-
869
- export const formatMarkdownPage = (title: string, source: string) => {
870
- const content = stripMatchingLeadingH1(source, title);
871
- if (!content) {
872
- return `# ${title}`;
873
- }
874
-
875
- return `# ${title}\n\n${content}`;
876
- };
877
-
878
- export const formatMarkdownPageSection = (
879
- title: string,
880
- url: string,
881
- source: string
882
- ) => {
883
- const content = stripMatchingLeadingH1(source, title);
884
- if (!content) {
885
- return `# ${title} (${url})`;
886
- }
887
-
888
- return `# ${title} (${url})\n\n${content}`;
889
- };
890
-
891
- const shouldIncludeSearchEntry = (
892
- entry: ContentEntry,
893
- pageMetadataMap: Map<string, PageMetadata>,
894
- config: SiteConfig
895
- ) => {
896
- const pageMeta = pageMetadataMap.get(entry.slug);
897
-
898
- if (pageMeta?.hidden || pageMeta?.noindex) {
899
- return false;
900
- }
901
-
902
- if (
903
- config.seo?.indexing !== "all" &&
904
- entry.kind === "entry" &&
905
- entry.hidden === true
906
- ) {
907
- return false;
908
- }
909
-
910
- return true;
911
- };
912
-
913
- const stripFrontmatter = (source: string) =>
914
- parseFrontmatter(source).body.trim();
915
-
916
- const getDocsCollection = (config: SiteConfig) =>
917
- config.collections.find((collection) => collection.type === "docs");
918
-
919
- const getDocsNavigation = (config: SiteConfig) =>
920
- getDocsCollection(config)?.navigation ?? config.navigation;
921
-
922
- const getDocsCollectionWithNavigation = (
923
- config: SiteConfig
924
- ): SiteConfig["collections"][number] | undefined => {
925
- const docsCollection = getDocsCollection(config);
926
- const docsNavigation = getDocsNavigation(config);
927
-
928
- return docsCollection &&
929
- docsNavigation &&
930
- docsCollection.navigation !== docsNavigation
931
- ? { ...docsCollection, navigation: docsNavigation }
932
- : docsCollection;
933
- };
934
-
935
- const getOpenApiSourceKey = (source: DocsOpenApiSource): string =>
936
- `${source.source}::${source.directory ?? ""}::${(source.include ?? []).join(
937
- "|"
938
- )}`;
939
-
940
- const toOpenApiSourceObject = (
941
- value: string | DocsOpenApiSource
942
- ): DocsOpenApiSource => {
943
- if (typeof value === "string") {
944
- return { source: value };
945
- }
946
- return value;
947
- };
948
-
949
- const collectOpenApiSources = (collection?: CollectionConfig) => {
950
- const sources: DocsOpenApiSource[] = [];
951
-
952
- for (const item of ensureArray(collection?.openapi)) {
953
- if (!item) {
954
- continue;
955
- }
956
- sources.push(toOpenApiSourceObject(item));
957
- }
958
-
959
- const groups = collection?.navigation?.groups ?? [];
960
- for (const group of groups) {
961
- if (!group.openapi) {
962
- continue;
963
- }
964
- sources.push(toOpenApiSourceObject(group.openapi));
965
- }
966
-
967
- const seen = new Set<string>();
968
- return sources.filter((source) => {
969
- const key = getOpenApiSourceKey(source);
970
- if (seen.has(key)) {
971
- return false;
972
- }
973
- seen.add(key);
974
- return true;
975
- });
976
- };
977
-
978
- const formatOpenApiPageContent = (operation: OpenApiOperation): string => {
979
- const parts = [`Method: ${operation.method}`, `Path: ${operation.path}`];
980
-
981
- if (operation.description) {
982
- parts.push(operation.description);
983
- }
984
- if (operation.tags.length) {
985
- parts.push(`Tags: ${operation.tags.join(", ")}`);
986
- }
987
- if (operation.parameters.length) {
988
- parts.push(`Parameters:\n${JSON.stringify(operation.parameters, null, 2)}`);
989
- }
990
- if (operation.requestBody) {
991
- parts.push(
992
- `Request Body:\n${JSON.stringify(operation.requestBody, null, 2)}`
993
- );
994
- }
995
- if (operation.responses) {
996
- parts.push(`Responses:\n${JSON.stringify(operation.responses, null, 2)}`);
997
- }
998
-
999
- return parts.join("\n\n");
1000
- };
1001
-
1002
- const getGroupedOpenApiSourceKey = (
1003
- source: string | DocsOpenApiSource
1004
- ): string => getOpenApiSourceKey(toOpenApiSourceObject(source));
1005
-
1006
- const collectUtilityOpenApiPages = (
1007
- pagesByIdentifier: Map<string, UtilityOpenApiPage>,
1008
- pagesBySource: Map<string, UtilityOpenApiPage[]>,
1009
- operations: OpenApiOperation[],
1010
- directory: string,
1011
- openApiSource: DocsOpenApiSource,
1012
- slugPrefix: string
1013
- ) => {
1014
- const sourceKey = getOpenApiSourceKey(openApiSource);
1015
- const includeIdentifiers = openApiSource.include?.length
1016
- ? new Set(openApiSource.include)
1017
- : null;
1018
-
1019
- for (const operation of operations) {
1020
- const identifier = openApiIdentifier(operation.method, operation.path);
1021
- if (includeIdentifiers && !includeIdentifiers.has(identifier)) {
1022
- continue;
1023
- }
1024
-
1025
- const baseSlug = normalizePath(
1026
- openApiSlug(operation.method, operation.path, directory)
1027
- );
1028
- const slug = slugPrefix
1029
- ? normalizePath(`${slugPrefix}/${baseSlug}`)
1030
- : baseSlug;
1031
- const page = {
1032
- content: formatOpenApiPageContent(operation),
1033
- description: operation.description,
1034
- identifier,
1035
- slug,
1036
- sourceKey,
1037
- title: operation.summary ?? identifier,
1038
- } satisfies UtilityOpenApiPage;
1039
-
1040
- pagesByIdentifier.set(identifier, page);
1041
- if (!pagesBySource.has(sourceKey)) {
1042
- pagesBySource.set(sourceKey, []);
1043
- }
1044
- pagesBySource.get(sourceKey)?.push(page);
1045
- }
1046
- };
1047
-
1048
- const addUtilityPagesFromSourceKey = (
1049
- pages: Map<string, UtilityPage>,
1050
- pagesBySource: Map<string, UtilityOpenApiPage[]>,
1051
- sourceKey: string
1052
- ) => {
1053
- for (const page of pagesBySource.get(sourceKey) ?? []) {
1054
- pages.set(page.slug, page);
1055
- }
1056
- };
1057
-
1058
- const addReferencedUtilityPages = (
1059
- pages: Map<string, UtilityPage>,
1060
- pagesByIdentifier: Map<string, UtilityOpenApiPage>,
1061
- pageReferences: string[] | undefined,
1062
- hiddenPages: Set<string>,
1063
- groupHidden = false
1064
- ) => {
1065
- for (const pageReference of pageReferences ?? []) {
1066
- if (groupHidden || hiddenPages.has(pageReference)) {
1067
- continue;
1068
- }
1069
-
1070
- const page = pagesByIdentifier.get(pageReference);
1071
- if (page) {
1072
- pages.set(page.slug, page);
1073
- }
1074
- }
1075
- };
1076
-
1077
- const buildUtilityOpenApiPages = async (
1078
- config: SiteConfig,
1079
- collection: CollectionConfig | undefined,
1080
- source: ContentSource
1081
- ) => {
1082
- if (!collection || collection.type !== "docs") {
1083
- return [] satisfies UtilityPage[];
1084
- }
1085
-
1086
- const docsNavigation = getDocsNavigation(config);
1087
- const hiddenPages = new Set(docsNavigation?.hidden);
1088
- const slugPrefix = normalizePath(collection.slugPrefix ?? "");
1089
- const byIdentifier = new Map<string, UtilityOpenApiPage>();
1090
- const bySource = new Map<string, UtilityOpenApiPage[]>();
1091
- const pages = new Map<string, UtilityPage>();
1092
- const sources = collectOpenApiSources(collection);
1093
-
1094
- const resolved = await Promise.all(
1095
- sources.map(async (item) => {
1096
- const rawSpec = await source.readFile(item.source);
1097
- const spec = parseOpenApiSpec(rawSpec, item.source);
1098
- const directory = item.directory ?? "api";
1099
- const { operations } = extractOpenApiOperations(spec, directory);
1100
- return { directory, operations, source: item };
1101
- })
1102
- );
1103
-
1104
- for (const { directory, operations, source: openApiSource } of resolved) {
1105
- collectUtilityOpenApiPages(
1106
- byIdentifier,
1107
- bySource,
1108
- operations,
1109
- directory,
1110
- openApiSource,
1111
- slugPrefix
1112
- );
1113
- }
1114
-
1115
- for (const openApiSource of ensureArray(collection.openapi)) {
1116
- if (!openApiSource) {
1117
- continue;
1118
- }
1119
- addUtilityPagesFromSourceKey(
1120
- pages,
1121
- bySource,
1122
- getGroupedOpenApiSourceKey(openApiSource)
1123
- );
1124
- }
1125
-
1126
- for (const group of docsNavigation?.groups ?? []) {
1127
- const groupHidden = group.hidden === true;
1128
- addReferencedUtilityPages(
1129
- pages,
1130
- byIdentifier,
1131
- group.pages,
1132
- hiddenPages,
1133
- groupHidden
1134
- );
1135
-
1136
- if (groupHidden || !group.openapi) {
1137
- continue;
1138
- }
1139
- addUtilityPagesFromSourceKey(
1140
- pages,
1141
- bySource,
1142
- getGroupedOpenApiSourceKey(group.openapi)
1143
- );
1144
- }
1145
-
1146
- addReferencedUtilityPages(
1147
- pages,
1148
- byIdentifier,
1149
- docsNavigation?.pages,
1150
- hiddenPages
1151
- );
1152
-
1153
- return [...pages.values()];
1154
- };
1155
-
1156
- export const buildSearchIndex = (
1157
- index: ContentIndex,
1158
- config: SiteConfig,
1159
- utilityIndex?: UtilityIndex
1160
- ): SearchIndexItem[] => {
1161
- const pageMetadataMap = buildPageMetadataMap(index);
1162
- const items = new Map<string, SearchIndexItem>();
1163
-
1164
- for (const page of utilityIndex?.pages ?? []) {
1165
- items.set(page.slug, {
1166
- path: page.slug,
1167
- title: page.title,
1168
- });
1169
- }
1170
-
1171
- for (const entry of index.entries) {
1172
- if (!shouldIncludeSearchEntry(entry, pageMetadataMap, config)) {
1173
- continue;
1174
- }
1175
-
1176
- const pageMeta = pageMetadataMap.get(entry.slug);
1177
- items.set(entry.slug, {
1178
- href: pageMeta?.url,
1179
- path: entry.slug,
1180
- title: pageMeta?.sidebarTitle ?? entry.title,
1181
- });
1182
- }
1183
-
1184
- return [...items.values()];
1185
- };
1186
-
1187
- export const buildUtilityIndex = async (
1188
- index: ContentIndex,
1189
- source: ContentSource,
1190
- config: SiteConfig
1191
- ): Promise<UtilityIndex> => {
1192
- const pageMetadataMap = buildPageMetadataMap(index);
1193
- const pages = new Map<string, UtilityPage>();
1194
-
1195
- for (const entry of index.entries) {
1196
- if (entry.kind !== "entry") {
1197
- continue;
1198
- }
1199
-
1200
- if (!shouldIncludeSearchEntry(entry, pageMetadataMap, config)) {
1201
- continue;
1202
- }
1203
-
1204
- const rawContent = await source.readFile(entry.relativePath);
1205
- pages.set(entry.slug, {
1206
- content: stripFrontmatter(rawContent),
1207
- description: entry.description,
1208
- slug: entry.slug,
1209
- title: entry.title,
1210
- });
1211
- }
1212
-
1213
- for (const page of await buildUtilityOpenApiPages(
1214
- config,
1215
- getDocsCollectionWithNavigation(config),
1216
- source
1217
- )) {
1218
- pages.set(page.slug, page);
1219
- }
1220
-
1221
- const sortedPages = [...pages.values()];
1222
- // oxlint-disable-next-line eslint-plugin-unicorn/no-array-sort
1223
- sortedPages.sort((left, right) => left.slug.localeCompare(right.slug));
1224
-
1225
- return {
1226
- description: config.description,
1227
- name: config.name,
1228
- pages: sortedPages,
1229
- slug: config.slug,
1230
- };
1231
- };
1232
-
1233
- const toUtilityDocPath = (value: string) => {
1234
- const clean = normalizePath(value);
1235
- if (!clean || clean === "index") {
1236
- return "/";
1237
- }
1238
- return `/${clean}`;
1239
- };
1240
-
1241
- const toUtilityTemplatedDocUrl = (value: string) =>
1242
- `${UTILITY_DOCS_ROOT_TOKEN}${toUtilityDocPath(value)}`;
1243
-
1244
- export const getPrebuiltUtilityLlmPagePath = (slug: string) => {
1245
- const normalized = normalizePath(slug);
1246
- return `_utility/llms-pages/${normalized || "index"}.mdx`;
1247
- };
1248
-
1249
- export const buildUtilityArtifacts = (
1250
- index: UtilityIndex
1251
- ): UtilityArtifact[] => {
1252
- const llmsLines = [
1253
- `# ${index.name}`,
1254
- index.description ? `> ${index.description}` : null,
1255
- "",
1256
- `Sitemap: ${toUtilityTemplatedDocUrl("sitemap.xml")}`,
1257
- `Skills: ${UTILITY_DOCS_ROOT_TOKEN}/.well-known/skills/index.json`,
1258
- "",
1259
- "## Docs",
1260
- ...index.pages.map((page) => {
1261
- const description = page.description ? `: ${page.description}` : "";
1262
- return `- [${page.title}](${toUtilityTemplatedDocUrl(page.slug)})${description}`;
1263
- }),
1264
- ];
1265
-
1266
- const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
1267
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1268
- ${index.pages
1269
- .map(
1270
- (page) => ` <url><loc>${toUtilityTemplatedDocUrl(page.slug)}</loc></url>`
1271
- )
1272
- .join("\n")}
1273
- </urlset>`;
1274
-
1275
- const llmsFull = index.pages
1276
- .map((page) =>
1277
- formatMarkdownPageSection(
1278
- page.title,
1279
- toUtilityTemplatedDocUrl(page.slug),
1280
- page.content
1281
- )
1282
- )
1283
- .join("\n\n");
1284
-
1285
- const skillSlug = index.slug ?? slugify(index.name);
1286
- const skillDescription =
1287
- `${index.name} documentation. ${index.description ?? ""}`.trim();
1288
- const skillsIndex = JSON.stringify(
1289
- {
1290
- skills: [
1291
- {
1292
- description: skillDescription,
1293
- files: ["SKILL.md"],
1294
- name: skillSlug,
1295
- },
1296
- ],
1297
- },
1298
- null,
1299
- 2
1300
- );
1301
-
1302
- const topPages = index.pages.slice(0, 20);
1303
- const skillMdLines = [
1304
- "---",
1305
- `name: ${skillSlug}`,
1306
- `description: ${skillDescription} Use when working with ${index.name}, answering questions about its features, or helping users follow its guides.`,
1307
- "---",
1308
- "",
1309
- `# ${index.name}`,
1310
- "",
1311
- index.description ? `${index.description}\n` : "",
1312
- "## Documentation",
1313
- "",
1314
- `- Full docs index: ${UTILITY_DOCS_ROOT_TOKEN}/llms.txt`,
1315
- `- Complete docs content: ${UTILITY_DOCS_ROOT_TOKEN}/llms-full.txt`,
1316
- "- Append `.md` to any page URL for raw markdown",
1317
- "",
1318
- "## Key Pages",
1319
- "",
1320
- ...topPages.map((page) => {
1321
- const desc = page.description ? ` - ${page.description}` : "";
1322
- return `- [${page.title}](${toUtilityTemplatedDocUrl(page.slug)})${desc}`;
1323
- }),
1324
- ];
1325
-
1326
- return [
1327
- {
1328
- content: sitemap,
1329
- contentType: "application/xml; charset=utf-8",
1330
- path: PREBUILT_UTILITY_SITEMAP_PATH,
1331
- },
1332
- {
1333
- content: llmsLines.filter((line) => line !== null).join("\n"),
1334
- contentType: "text/plain; charset=utf-8",
1335
- path: PREBUILT_UTILITY_LLMS_PATH,
1336
- },
1337
- {
1338
- content: llmsFull,
1339
- contentType: "text/plain; charset=utf-8",
1340
- path: PREBUILT_UTILITY_LLMS_FULL_PATH,
1341
- },
1342
- {
1343
- content: skillsIndex,
1344
- contentType: "application/json; charset=utf-8",
1345
- path: PREBUILT_UTILITY_SKILLS_INDEX_PATH,
1346
- },
1347
- {
1348
- content: skillMdLines.filter((line) => line !== null).join("\n"),
1349
- contentType: "text/markdown; charset=utf-8",
1350
- path: `${PREBUILT_UTILITY_SKILLS_MD_PREFIX}${skillSlug}/SKILL.md`,
1351
- },
1352
- ...index.pages.map((page) => ({
1353
- content: formatMarkdownPage(page.title, page.content),
1354
- contentType: "text/markdown; charset=utf-8",
1355
- path: getPrebuiltUtilityLlmPagePath(page.slug),
1356
- })),
1357
- ];
1358
- };
1359
-
1360
- export const buildTocIndex = async (
1361
- index: ContentIndex,
1362
- source: ContentSource
1363
- ): Promise<Map<string, TocItem[]>> => {
1364
- const itemsBySlug = new Map<string, TocItem[]>();
1365
-
1366
- for (const entry of index.entries) {
1367
- if (entry.kind !== "entry") {
1368
- continue;
1369
- }
1370
-
1371
- const rawContent = await source.readFile(entry.relativePath);
1372
- itemsBySlug.set(entry.slug, extractToc(rawContent));
1373
- }
1374
-
1375
- return itemsBySlug;
1376
- };
1377
-
1378
- export const serializeContentIndex = (index: ContentIndex): string =>
1379
- JSON.stringify({
1380
- collections: Object.fromEntries(index.byCollection),
1381
- entries: index.entries,
1382
- version: 1,
1383
- } satisfies SerializedContentIndex);
1384
-
1385
- export const serializeOpenApiIndex = (
1386
- entries: PrebuiltOpenApiEntry[]
1387
- ): string =>
1388
- JSON.stringify({
1389
- entries,
1390
- version: 1,
1391
- } satisfies SerializedOpenApiIndex);
1392
-
1393
- export const loadPrebuiltContentIndex = async (
1394
- source: ContentSource
1395
- ): Promise<ContentIndex | null> => {
1396
- try {
1397
- const raw = await source.readFile(PREBUILT_INDEX_PATH);
1398
- const data = JSON.parse(raw) as SerializedContentIndex;
1399
- if (data.version !== 1 || !Array.isArray(data.entries)) {
1400
- return null;
1401
- }
1402
-
1403
- const bySlug = new Map<string, ContentEntry>();
1404
- const byCollection = new Map<string, ContentEntry[]>();
1405
-
1406
- for (const entry of data.entries) {
1407
- bySlug.set(entry.slug, entry);
1408
- }
1409
-
1410
- for (const [collectionId, entries] of Object.entries(
1411
- data.collections ?? {}
1412
- )) {
1413
- byCollection.set(collectionId, entries);
1414
- }
1415
-
1416
- return {
1417
- byCollection,
1418
- bySlug,
1419
- entries: data.entries,
1420
- errors: [],
1421
- };
1422
- } catch {
1423
- return null;
1424
- }
1425
- };
1426
-
1427
- export const loadPrebuiltOpenApiIndex = async (
1428
- source: ContentSource
1429
- ): Promise<PrebuiltOpenApiEntry[] | null> => {
1430
- try {
1431
- const raw = await source.readFile(PREBUILT_OPENAPI_INDEX_PATH);
1432
- const data = JSON.parse(raw) as SerializedOpenApiIndex;
1433
- if (data.version !== 1 || !Array.isArray(data.entries)) {
1434
- return null;
1435
- }
1436
-
1437
- return data.entries;
1438
- } catch {
1439
- return null;
1440
- }
1441
- };
1442
-
1443
- export const serializeSearchIndex = (items: SearchIndexItem[]): string =>
1444
- JSON.stringify({
1445
- items,
1446
- version: 1,
1447
- } satisfies SerializedSearchIndex);
1448
-
1449
- export const loadPrebuiltSearchIndex = async (
1450
- source: ContentSource
1451
- ): Promise<SearchIndexItem[] | null> => {
1452
- try {
1453
- const raw = await source.readFile(PREBUILT_SEARCH_INDEX_PATH);
1454
- const data = JSON.parse(raw) as SerializedSearchIndex;
1455
- if (data.version !== 1 || !Array.isArray(data.items)) {
1456
- return null;
1457
- }
1458
-
1459
- return data.items;
1460
- } catch {
1461
- return null;
1462
- }
1463
- };
1464
-
1465
- export const serializeTocIndex = (
1466
- itemsBySlug: Map<string, TocItem[]>
1467
- ): string =>
1468
- JSON.stringify({
1469
- itemsBySlug: Object.fromEntries(itemsBySlug),
1470
- version: 1,
1471
- } satisfies SerializedTocIndex);
1472
-
1473
- export const loadPrebuiltTocIndex = async (
1474
- source: ContentSource
1475
- ): Promise<Map<string, TocItem[]> | null> => {
1476
- try {
1477
- const raw = await source.readFile(PREBUILT_TOC_INDEX_PATH);
1478
- const data = JSON.parse(raw) as SerializedTocIndex;
1479
- if (data.version !== 1 || typeof data.itemsBySlug !== "object") {
1480
- return null;
1481
- }
1482
-
1483
- return new Map(Object.entries(data.itemsBySlug ?? {}));
1484
- } catch {
1485
- return null;
1486
- }
1487
- };
1488
-
1489
- export const serializeUtilityIndex = (index: UtilityIndex): string =>
1490
- JSON.stringify({
1491
- ...index,
1492
- version: 1,
1493
- } satisfies SerializedUtilityIndex);
1494
-
1495
- export const loadPrebuiltUtilityIndex = async (
1496
- source: ContentSource
1497
- ): Promise<UtilityIndex | null> => {
1498
- try {
1499
- const raw = await source.readFile(PREBUILT_UTILITY_INDEX_PATH);
1500
- const data = JSON.parse(raw) as SerializedUtilityIndex;
1501
- if (
1502
- data.version !== 1 ||
1503
- typeof data.name !== "string" ||
1504
- !Array.isArray(data.pages)
1505
- ) {
1506
- return null;
1507
- }
1508
-
1509
- return {
1510
- description: data.description,
1511
- name: data.name,
1512
- pages: data.pages,
1513
- };
1514
- } catch {
1515
- return null;
1516
- }
1517
- };
25
+ export { createFsSource, FsContentSource } from "./fs-source.js";
26
+ export {
27
+ prepareLlmsFullContent,
28
+ toAgentMarkdown,
29
+ } from "./markdown/agent-markdown.js";
30
+ export {
31
+ formatMarkdownPage,
32
+ formatMarkdownPageSection,
33
+ stripFrontmatter,
34
+ } from "./markdown/format.js";
35
+ export {
36
+ absolutiseInternalLinks,
37
+ sanitizePlaceholderUrls,
38
+ } from "./markdown/links.js";
39
+ export { formatOpenApiPageContent } from "./openapi-pages.js";
40
+ export { buildSearchIndex } from "./search-index.js";
41
+ export {
42
+ loadPrebuiltContentIndex,
43
+ loadPrebuiltOpenApiIndex,
44
+ loadPrebuiltSearchIndex,
45
+ loadPrebuiltTocIndex,
46
+ loadPrebuiltUtilityIndex,
47
+ serializeContentIndex,
48
+ serializeOpenApiIndex,
49
+ serializeSearchIndex,
50
+ serializeTocIndex,
51
+ serializeUtilityIndex,
52
+ } from "./serialization.js";
53
+ export { buildTocIndex, extractToc } from "./toc-index.js";
54
+ export type {
55
+ ContentEntry,
56
+ ContentIndex,
57
+ PageMetadata,
58
+ PrebuiltOpenApiEntry,
59
+ SearchIndexItem,
60
+ SiteConfigResult,
61
+ TocItem,
62
+ UtilityArtifact,
63
+ UtilityIndex,
64
+ UtilityPage,
65
+ UtilitySegment,
66
+ } from "./types.js";
67
+ export {
68
+ buildUtilityArtifacts,
69
+ buildUtilityIndex,
70
+ getPrebuiltUtilityLlmPagePath,
71
+ } from "./utility-index.js";