blodemd 0.0.12 → 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 (138) hide show
  1. package/README.md +1 -1
  2. package/dev-server/lib/local-runtime.tsx +12 -15
  3. package/dev-server/next.config.js +19 -9
  4. package/dev-server/package.json +5 -1
  5. package/dev-server/tsconfig.json +0 -3
  6. package/dist/cli.mjs +1205 -969
  7. package/dist/cli.mjs.map +1 -1
  8. package/docs/app/globals.css +1 -1
  9. package/docs/components/api/api-playground.tsx +2 -5
  10. package/docs/components/api/api-reference.tsx +29 -3
  11. package/docs/components/docs/contextual-menu.tsx +37 -33
  12. package/docs/components/docs/copy-page-menu.tsx +18 -0
  13. package/docs/components/docs/doc-header.tsx +4 -1
  14. package/docs/components/docs/doc-shell.tsx +48 -18
  15. package/docs/components/docs/doc-sidebar.tsx +131 -89
  16. package/docs/components/docs/doc-toc.tsx +1 -0
  17. package/docs/components/docs/mobile-nav.tsx +72 -68
  18. package/docs/components/docs/sidebar-active-highlight.tsx +34 -0
  19. package/docs/components/docs/sidebar-scroll-area.tsx +9 -4
  20. package/docs/components/git/repo-picker.tsx +5 -2
  21. package/docs/components/mdx/index.tsx +31 -21
  22. package/docs/components/posthog-provider.tsx +42 -0
  23. package/docs/components/tenant-analytics.tsx +49 -0
  24. package/docs/components/third-parties.tsx +4 -1
  25. package/docs/components/ui/copy-button.tsx +16 -2
  26. package/docs/components/ui/search-dialog.tsx +431 -0
  27. package/docs/components/ui/search.tsx +44 -427
  28. package/docs/components/web-mcp.tsx +115 -0
  29. package/docs/lib/agent-skills.ts +48 -0
  30. package/docs/lib/config.ts +4 -5
  31. package/docs/lib/docs-collection.ts +7 -22
  32. package/docs/lib/docs-runtime.tsx +160 -10
  33. package/docs/lib/llms-full.ts +82 -0
  34. package/docs/lib/marketing-markdown.ts +79 -0
  35. package/docs/lib/marketing-site.ts +31 -0
  36. package/docs/lib/mdx.ts +17 -6
  37. package/docs/lib/routes.ts +19 -3
  38. package/docs/lib/shiki.ts +4 -0
  39. package/docs/lib/tenancy.ts +15 -2
  40. package/docs/lib/tenant-headers.ts +29 -0
  41. package/docs/lib/tenant-static.ts +73 -99
  42. package/package.json +10 -5
  43. package/packages/@repo/common/package.json +3 -3
  44. package/packages/@repo/contracts/dist/analytics.d.ts +21 -0
  45. package/packages/@repo/contracts/dist/analytics.d.ts.map +1 -0
  46. package/packages/@repo/contracts/dist/analytics.js +18 -0
  47. package/packages/@repo/contracts/dist/deployment.js +1 -1
  48. package/packages/@repo/contracts/dist/git.js +2 -2
  49. package/packages/@repo/contracts/dist/ids.d.ts.map +1 -1
  50. package/packages/@repo/contracts/dist/ids.js +2 -1
  51. package/packages/@repo/contracts/dist/index.d.ts +1 -0
  52. package/packages/@repo/contracts/dist/index.d.ts.map +1 -1
  53. package/packages/@repo/contracts/dist/index.js +1 -0
  54. package/packages/@repo/contracts/dist/project.d.ts +18 -1
  55. package/packages/@repo/contracts/dist/project.d.ts.map +1 -1
  56. package/packages/@repo/contracts/dist/project.js +3 -1
  57. package/packages/@repo/contracts/dist/tenant.d.ts +36 -0
  58. package/packages/@repo/contracts/dist/tenant.d.ts.map +1 -1
  59. package/packages/@repo/contracts/dist/tenant.js +2 -0
  60. package/packages/@repo/contracts/package.json +3 -3
  61. package/packages/@repo/contracts/src/analytics.ts +33 -0
  62. package/packages/@repo/contracts/src/deployment.ts +1 -1
  63. package/packages/@repo/contracts/src/git.ts +2 -2
  64. package/packages/@repo/contracts/src/ids.ts +2 -1
  65. package/packages/@repo/contracts/src/index.ts +1 -0
  66. package/packages/@repo/contracts/src/project.ts +3 -1
  67. package/packages/@repo/contracts/src/tenant.ts +2 -0
  68. package/packages/@repo/models/dist/docs-config.d.ts +0 -198
  69. package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
  70. package/packages/@repo/models/dist/docs-config.js +3 -2
  71. package/packages/@repo/models/dist/tenant.d.ts +12 -0
  72. package/packages/@repo/models/dist/tenant.d.ts.map +1 -1
  73. package/packages/@repo/models/package.json +3 -3
  74. package/packages/@repo/models/src/docs-config.ts +3 -3
  75. package/packages/@repo/models/src/tenant.ts +15 -0
  76. package/packages/@repo/prebuild/dist/openapi.d.ts.map +1 -1
  77. package/packages/@repo/prebuild/dist/openapi.js +32 -4
  78. package/packages/@repo/prebuild/package.json +3 -3
  79. package/packages/@repo/prebuild/src/openapi.ts +42 -4
  80. package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
  81. package/packages/@repo/previewing/dist/blob-source.js +18 -5
  82. package/packages/@repo/previewing/dist/config-loader.d.ts +134 -0
  83. package/packages/@repo/previewing/dist/config-loader.d.ts.map +1 -0
  84. package/packages/@repo/previewing/dist/config-loader.js +231 -0
  85. package/packages/@repo/previewing/dist/constants.d.ts +15 -0
  86. package/packages/@repo/previewing/dist/constants.d.ts.map +1 -0
  87. package/packages/@repo/previewing/dist/constants.js +14 -0
  88. package/packages/@repo/previewing/dist/content-index.d.ts +7 -0
  89. package/packages/@repo/previewing/dist/content-index.d.ts.map +1 -0
  90. package/packages/@repo/previewing/dist/content-index.js +291 -0
  91. package/packages/@repo/previewing/dist/index.d.ts +13 -121
  92. package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
  93. package/packages/@repo/previewing/dist/index.js +11 -1003
  94. package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts +3 -0
  95. package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts.map +1 -0
  96. package/packages/@repo/previewing/dist/markdown/agent-markdown.js +176 -0
  97. package/packages/@repo/previewing/dist/markdown/format.d.ts +8 -0
  98. package/packages/@repo/previewing/dist/markdown/format.d.ts.map +1 -0
  99. package/packages/@repo/previewing/dist/markdown/format.js +43 -0
  100. package/packages/@repo/previewing/dist/markdown/links.d.ts +3 -0
  101. package/packages/@repo/previewing/dist/markdown/links.d.ts.map +1 -0
  102. package/packages/@repo/previewing/dist/markdown/links.js +31 -0
  103. package/packages/@repo/previewing/dist/openapi-pages.d.ts +7 -0
  104. package/packages/@repo/previewing/dist/openapi-pages.d.ts.map +1 -0
  105. package/packages/@repo/previewing/dist/openapi-pages.js +200 -0
  106. package/packages/@repo/previewing/dist/search-index.d.ts +4 -0
  107. package/packages/@repo/previewing/dist/search-index.d.ts.map +1 -0
  108. package/packages/@repo/previewing/dist/search-index.js +23 -0
  109. package/packages/@repo/previewing/dist/serialization.d.ts +13 -0
  110. package/packages/@repo/previewing/dist/serialization.d.ts.map +1 -0
  111. package/packages/@repo/previewing/dist/serialization.js +108 -0
  112. package/packages/@repo/previewing/dist/toc-index.d.ts +5 -0
  113. package/packages/@repo/previewing/dist/toc-index.d.ts.map +1 -0
  114. package/packages/@repo/previewing/dist/toc-index.js +53 -0
  115. package/packages/@repo/previewing/dist/types.d.ts +92 -0
  116. package/packages/@repo/previewing/dist/types.d.ts.map +1 -0
  117. package/packages/@repo/previewing/dist/types.js +1 -0
  118. package/packages/@repo/previewing/dist/utility-index.d.ts +7 -0
  119. package/packages/@repo/previewing/dist/utility-index.d.ts.map +1 -0
  120. package/packages/@repo/previewing/dist/utility-index.js +201 -0
  121. package/packages/@repo/previewing/package.json +3 -3
  122. package/packages/@repo/previewing/src/blob-source.ts +20 -5
  123. package/packages/@repo/previewing/src/config-loader.ts +316 -0
  124. package/packages/@repo/previewing/src/constants.ts +16 -0
  125. package/packages/@repo/previewing/src/content-index.ts +384 -0
  126. package/packages/@repo/previewing/src/index.ts +69 -1515
  127. package/packages/@repo/previewing/src/markdown/agent-markdown.ts +254 -0
  128. package/packages/@repo/previewing/src/markdown/format.ts +63 -0
  129. package/packages/@repo/previewing/src/markdown/links.ts +46 -0
  130. package/packages/@repo/previewing/src/openapi-pages.ts +319 -0
  131. package/packages/@repo/previewing/src/search-index.ts +38 -0
  132. package/packages/@repo/previewing/src/serialization.ts +191 -0
  133. package/packages/@repo/previewing/src/toc-index.ts +72 -0
  134. package/packages/@repo/previewing/src/types.ts +105 -0
  135. package/packages/@repo/previewing/src/utility-index.ts +304 -0
  136. package/packages/@repo/validation/package.json +3 -3
  137. package/scripts/repo-packages.mjs +8 -0
  138. package/docs/components/ui/site-footer.tsx +0 -41
@@ -0,0 +1,38 @@
1
+ import type { SiteConfig } from "@repo/models";
2
+
3
+ import {
4
+ buildPageMetadataMap,
5
+ shouldIncludeSearchEntry,
6
+ } from "./content-index.js";
7
+ import type { ContentIndex, SearchIndexItem, UtilityIndex } from "./types.js";
8
+
9
+ export const buildSearchIndex = (
10
+ index: ContentIndex,
11
+ config: SiteConfig,
12
+ utilityIndex?: UtilityIndex
13
+ ): SearchIndexItem[] => {
14
+ const pageMetadataMap = buildPageMetadataMap(index);
15
+ const items = new Map<string, SearchIndexItem>();
16
+
17
+ for (const page of utilityIndex?.pages ?? []) {
18
+ items.set(page.slug, {
19
+ path: page.slug,
20
+ title: page.title,
21
+ });
22
+ }
23
+
24
+ for (const entry of index.entries) {
25
+ if (!shouldIncludeSearchEntry(entry, pageMetadataMap, config)) {
26
+ continue;
27
+ }
28
+
29
+ const pageMeta = pageMetadataMap.get(entry.slug);
30
+ items.set(entry.slug, {
31
+ href: pageMeta?.url,
32
+ path: entry.slug,
33
+ title: pageMeta?.sidebarTitle ?? entry.title,
34
+ });
35
+ }
36
+
37
+ return [...items.values()];
38
+ };
@@ -0,0 +1,191 @@
1
+ import {
2
+ PREBUILT_INDEX_PATH,
3
+ PREBUILT_OPENAPI_INDEX_PATH,
4
+ PREBUILT_SEARCH_INDEX_PATH,
5
+ PREBUILT_TOC_INDEX_PATH,
6
+ PREBUILT_UTILITY_INDEX_PATH,
7
+ } from "./constants.js";
8
+ import type { ContentSource } from "./content-source.js";
9
+ import type {
10
+ ContentEntry,
11
+ ContentIndex,
12
+ PrebuiltOpenApiEntry,
13
+ SearchIndexItem,
14
+ TocItem,
15
+ UtilityIndex,
16
+ UtilityPage,
17
+ UtilitySegment,
18
+ } from "./types.js";
19
+
20
+ interface SerializedContentIndex {
21
+ version: 1;
22
+ entries: ContentEntry[];
23
+ collections: Record<string, ContentEntry[]>;
24
+ }
25
+
26
+ interface SerializedOpenApiIndex {
27
+ entries: PrebuiltOpenApiEntry[];
28
+ version: 1;
29
+ }
30
+
31
+ interface SerializedSearchIndex {
32
+ items: SearchIndexItem[];
33
+ version: 1;
34
+ }
35
+
36
+ interface SerializedTocIndex {
37
+ itemsBySlug: Record<string, TocItem[]>;
38
+ version: 1;
39
+ }
40
+
41
+ interface SerializedUtilityIndex {
42
+ description?: string;
43
+ name: string;
44
+ pages: UtilityPage[];
45
+ segments?: UtilitySegment[];
46
+ slug?: string;
47
+ version: 1;
48
+ }
49
+
50
+ export const serializeContentIndex = (index: ContentIndex): string =>
51
+ JSON.stringify({
52
+ collections: Object.fromEntries(index.byCollection),
53
+ entries: index.entries,
54
+ version: 1,
55
+ } satisfies SerializedContentIndex);
56
+
57
+ export const serializeOpenApiIndex = (
58
+ entries: PrebuiltOpenApiEntry[]
59
+ ): string =>
60
+ JSON.stringify({
61
+ entries,
62
+ version: 1,
63
+ } satisfies SerializedOpenApiIndex);
64
+
65
+ export const loadPrebuiltContentIndex = async (
66
+ source: ContentSource
67
+ ): Promise<ContentIndex | null> => {
68
+ try {
69
+ const raw = await source.readFile(PREBUILT_INDEX_PATH);
70
+ const data = JSON.parse(raw) as SerializedContentIndex;
71
+ if (data.version !== 1 || !Array.isArray(data.entries)) {
72
+ return null;
73
+ }
74
+
75
+ const bySlug = new Map<string, ContentEntry>();
76
+ const byCollection = new Map<string, ContentEntry[]>();
77
+
78
+ for (const entry of data.entries) {
79
+ bySlug.set(entry.slug, entry);
80
+ }
81
+
82
+ for (const [collectionId, entries] of Object.entries(
83
+ data.collections ?? {}
84
+ )) {
85
+ byCollection.set(collectionId, entries);
86
+ }
87
+
88
+ return {
89
+ byCollection,
90
+ bySlug,
91
+ entries: data.entries,
92
+ errors: [],
93
+ };
94
+ } catch {
95
+ return null;
96
+ }
97
+ };
98
+
99
+ export const loadPrebuiltOpenApiIndex = async (
100
+ source: ContentSource
101
+ ): Promise<PrebuiltOpenApiEntry[] | null> => {
102
+ try {
103
+ const raw = await source.readFile(PREBUILT_OPENAPI_INDEX_PATH);
104
+ const data = JSON.parse(raw) as SerializedOpenApiIndex;
105
+ if (data.version !== 1 || !Array.isArray(data.entries)) {
106
+ return null;
107
+ }
108
+
109
+ return data.entries;
110
+ } catch {
111
+ return null;
112
+ }
113
+ };
114
+
115
+ export const serializeSearchIndex = (items: SearchIndexItem[]): string =>
116
+ JSON.stringify({
117
+ items,
118
+ version: 1,
119
+ } satisfies SerializedSearchIndex);
120
+
121
+ export const loadPrebuiltSearchIndex = async (
122
+ source: ContentSource
123
+ ): Promise<SearchIndexItem[] | null> => {
124
+ try {
125
+ const raw = await source.readFile(PREBUILT_SEARCH_INDEX_PATH);
126
+ const data = JSON.parse(raw) as SerializedSearchIndex;
127
+ if (data.version !== 1 || !Array.isArray(data.items)) {
128
+ return null;
129
+ }
130
+
131
+ return data.items;
132
+ } catch {
133
+ return null;
134
+ }
135
+ };
136
+
137
+ export const serializeTocIndex = (
138
+ itemsBySlug: Map<string, TocItem[]>
139
+ ): string =>
140
+ JSON.stringify({
141
+ itemsBySlug: Object.fromEntries(itemsBySlug),
142
+ version: 1,
143
+ } satisfies SerializedTocIndex);
144
+
145
+ export const loadPrebuiltTocIndex = async (
146
+ source: ContentSource
147
+ ): Promise<Map<string, TocItem[]> | null> => {
148
+ try {
149
+ const raw = await source.readFile(PREBUILT_TOC_INDEX_PATH);
150
+ const data = JSON.parse(raw) as SerializedTocIndex;
151
+ if (data.version !== 1 || typeof data.itemsBySlug !== "object") {
152
+ return null;
153
+ }
154
+
155
+ return new Map(Object.entries(data.itemsBySlug ?? {}));
156
+ } catch {
157
+ return null;
158
+ }
159
+ };
160
+
161
+ export const serializeUtilityIndex = (index: UtilityIndex): string =>
162
+ JSON.stringify({
163
+ ...index,
164
+ version: 1,
165
+ } satisfies SerializedUtilityIndex);
166
+
167
+ export const loadPrebuiltUtilityIndex = async (
168
+ source: ContentSource
169
+ ): Promise<UtilityIndex | null> => {
170
+ try {
171
+ const raw = await source.readFile(PREBUILT_UTILITY_INDEX_PATH);
172
+ const data = JSON.parse(raw) as SerializedUtilityIndex;
173
+ if (
174
+ data.version !== 1 ||
175
+ typeof data.name !== "string" ||
176
+ !Array.isArray(data.pages)
177
+ ) {
178
+ return null;
179
+ }
180
+
181
+ return {
182
+ description: data.description,
183
+ name: data.name,
184
+ pages: data.pages,
185
+ segments: Array.isArray(data.segments) ? data.segments : undefined,
186
+ slug: data.slug,
187
+ };
188
+ } catch {
189
+ return null;
190
+ }
191
+ };
@@ -0,0 +1,72 @@
1
+ import { slugify } from "@repo/common";
2
+
3
+ import { NEWLINE_REGEX } from "./constants.js";
4
+ import type { ContentSource } from "./content-source.js";
5
+ import type { ContentIndex, TocItem } from "./types.js";
6
+
7
+ const HEADING_REGEX = /^(#{2,4})\s+(.*)$/;
8
+ // Matches ``` and ~~~ fenced blocks, requiring the same fence to close them.
9
+ const TOC_FENCE_REGEX = /(`{3,}|~{3,})[\s\S]*?\1/g;
10
+
11
+ export const extractToc = (source: string): TocItem[] => {
12
+ const withoutCode = source.replaceAll(TOC_FENCE_REGEX, "");
13
+ const lines = withoutCode.split(NEWLINE_REGEX);
14
+ const toc: TocItem[] = [];
15
+ const emitted = new Set<string>();
16
+
17
+ for (const line of lines) {
18
+ const match = HEADING_REGEX.exec(line.trim());
19
+ if (!match) {
20
+ continue;
21
+ }
22
+
23
+ const [, hashes = "", heading = ""] = match;
24
+ if (!(hashes && heading)) {
25
+ continue;
26
+ }
27
+
28
+ const baseId = slugify(heading.trim());
29
+ // Increment until the id is unique among the ids we have already emitted,
30
+ // so a suffixed id never collides with an explicit "foo-1" heading.
31
+ let id = baseId;
32
+ let count = 1;
33
+ while (emitted.has(id)) {
34
+ id = `${baseId}-${count}`;
35
+ count += 1;
36
+ }
37
+ emitted.add(id);
38
+
39
+ toc.push({
40
+ id,
41
+ level: hashes.length,
42
+ title: heading.trim(),
43
+ });
44
+ }
45
+
46
+ return toc;
47
+ };
48
+
49
+ export const buildTocIndex = async (
50
+ index: ContentIndex,
51
+ source: ContentSource
52
+ ): Promise<Map<string, TocItem[]>> => {
53
+ const itemsBySlug = new Map<string, TocItem[]>();
54
+
55
+ const results = await Promise.all(
56
+ index.entries.map(async (entry) => {
57
+ if (entry.kind !== "entry") {
58
+ return null;
59
+ }
60
+ const rawContent = await source.readFile(entry.relativePath);
61
+ return { slug: entry.slug, toc: extractToc(rawContent) };
62
+ })
63
+ );
64
+
65
+ for (const result of results) {
66
+ if (result) {
67
+ itemsBySlug.set(result.slug, result.toc);
68
+ }
69
+ }
70
+
71
+ return itemsBySlug;
72
+ };
@@ -0,0 +1,105 @@
1
+ import type {
2
+ ContentType,
3
+ DocsOpenApiSource,
4
+ FrontmatterByType,
5
+ PageMode,
6
+ SiteConfig,
7
+ } from "@repo/models";
8
+ import type { OpenApiOperation, OpenApiSpec } from "@repo/prebuild";
9
+
10
+ export type SiteConfigResult =
11
+ | { ok: true; config: SiteConfig; warnings: string[] }
12
+ | { ok: false; errors: string[] };
13
+
14
+ export type ContentEntry =
15
+ | {
16
+ kind: "entry";
17
+ slug: string;
18
+ title: string;
19
+ description?: string;
20
+ hidden?: boolean;
21
+ type: ContentType;
22
+ collectionId: string;
23
+ sourcePath: string;
24
+ relativePath: string;
25
+ frontmatter: FrontmatterByType[ContentType];
26
+ }
27
+ | {
28
+ kind: "index";
29
+ slug: string;
30
+ title: string;
31
+ description?: string;
32
+ type: ContentType;
33
+ collectionId: string;
34
+ };
35
+
36
+ export interface ContentIndex {
37
+ entries: ContentEntry[];
38
+ bySlug: Map<string, ContentEntry>;
39
+ byCollection: Map<string, ContentEntry[]>;
40
+ errors: string[];
41
+ }
42
+
43
+ export interface PageMetadata {
44
+ title?: string;
45
+ sidebarTitle?: string;
46
+ icon?: string;
47
+ iconType?: string;
48
+ tag?: string;
49
+ hidden?: boolean;
50
+ deprecated?: boolean;
51
+ url?: string;
52
+ mode?: PageMode;
53
+ noindex?: boolean;
54
+ hideFooterPagination?: boolean;
55
+ hideApiMarker?: boolean;
56
+ keywords?: string[];
57
+ }
58
+
59
+ export interface SearchIndexItem {
60
+ href?: string;
61
+ title: string;
62
+ path: string;
63
+ }
64
+
65
+ export interface TocItem {
66
+ id: string;
67
+ level: number;
68
+ title: string;
69
+ }
70
+
71
+ export interface UtilityPage {
72
+ content: string;
73
+ description?: string;
74
+ slug: string;
75
+ title: string;
76
+ }
77
+
78
+ export interface UtilitySegment {
79
+ pageSlugs: string[];
80
+ slug: string;
81
+ title: string;
82
+ }
83
+
84
+ export interface UtilityIndex {
85
+ description?: string;
86
+ name: string;
87
+ pages: UtilityPage[];
88
+ segments?: UtilitySegment[];
89
+ slug?: string;
90
+ }
91
+
92
+ export interface UtilityArtifact {
93
+ content: string;
94
+ contentType: string;
95
+ path: string;
96
+ }
97
+
98
+ export interface PrebuiltOpenApiEntry {
99
+ identifier: string;
100
+ operation: OpenApiOperation;
101
+ slug: string;
102
+ source: DocsOpenApiSource;
103
+ sourceKey: string;
104
+ spec: OpenApiSpec;
105
+ }
@@ -0,0 +1,304 @@
1
+ import { normalizePath, slugify } from "@repo/common";
2
+ import type { SiteConfig } from "@repo/models";
3
+
4
+ import {
5
+ getDocsCollectionWithNavigation,
6
+ getDocsNavigation,
7
+ } from "./config-loader.js";
8
+ import {
9
+ PREBUILT_UTILITY_LLMS_FULL_PATH,
10
+ PREBUILT_UTILITY_LLMS_PATH,
11
+ PREBUILT_UTILITY_LLMS_SEGMENT_PREFIX,
12
+ PREBUILT_UTILITY_SITEMAP_PATH,
13
+ PREBUILT_UTILITY_SKILLS_INDEX_PATH,
14
+ PREBUILT_UTILITY_SKILLS_MD_PREFIX,
15
+ UTILITY_DOCS_ROOT_TOKEN,
16
+ } from "./constants.js";
17
+ import {
18
+ buildPageMetadataMap,
19
+ shouldIncludeSearchEntry,
20
+ } from "./content-index.js";
21
+ import type { ContentSource } from "./content-source.js";
22
+ import {
23
+ prepareLlmsFullContent,
24
+ toAgentMarkdown,
25
+ } from "./markdown/agent-markdown.js";
26
+ import {
27
+ formatMarkdownPage,
28
+ formatMarkdownPageSection,
29
+ stripFrontmatter,
30
+ } from "./markdown/format.js";
31
+ import { buildUtilityOpenApiPages } from "./openapi-pages.js";
32
+ import type {
33
+ ContentIndex,
34
+ UtilityArtifact,
35
+ UtilityIndex,
36
+ UtilityPage,
37
+ UtilitySegment,
38
+ } from "./types.js";
39
+
40
+ const collectUtilitySegments = (
41
+ config: SiteConfig,
42
+ pages: Map<string, UtilityPage>
43
+ ): UtilitySegment[] => {
44
+ const segments = new Map<string, UtilitySegment>();
45
+ const addGroup = (group: {
46
+ group?: string;
47
+ hidden?: boolean;
48
+ pages?: string[];
49
+ }) => {
50
+ if (group.hidden || !group.group) {
51
+ return;
52
+ }
53
+ const pageSlugs = (group.pages ?? []).filter((page) => pages.has(page));
54
+ if (pageSlugs.length === 0) {
55
+ return;
56
+ }
57
+ const slug = slugify(group.group);
58
+ segments.set(slug, { pageSlugs, slug, title: group.group });
59
+ };
60
+
61
+ const navigation = getDocsNavigation(config);
62
+ for (const group of navigation?.groups ?? []) {
63
+ addGroup(group);
64
+ }
65
+ for (const tab of navigation?.tabs ?? []) {
66
+ for (const group of tab.groups ?? []) {
67
+ addGroup(group);
68
+ }
69
+ }
70
+
71
+ return [...segments.values()];
72
+ };
73
+
74
+ export const buildUtilityIndex = async (
75
+ index: ContentIndex,
76
+ source: ContentSource,
77
+ config: SiteConfig
78
+ ): Promise<UtilityIndex> => {
79
+ const pageMetadataMap = buildPageMetadataMap(index);
80
+ const pages = new Map<string, UtilityPage>();
81
+
82
+ const contentPages = await Promise.all(
83
+ index.entries.map(async (entry) => {
84
+ if (entry.kind !== "entry") {
85
+ return null;
86
+ }
87
+ if (!shouldIncludeSearchEntry(entry, pageMetadataMap, config)) {
88
+ return null;
89
+ }
90
+ const rawContent = await source.readFile(entry.relativePath);
91
+ return {
92
+ content: stripFrontmatter(rawContent),
93
+ description: entry.description,
94
+ slug: entry.slug,
95
+ title: entry.title,
96
+ } satisfies UtilityPage;
97
+ })
98
+ );
99
+ for (const page of contentPages) {
100
+ if (page) {
101
+ pages.set(page.slug, page);
102
+ }
103
+ }
104
+
105
+ for (const page of await buildUtilityOpenApiPages(
106
+ config,
107
+ getDocsCollectionWithNavigation(config),
108
+ source
109
+ )) {
110
+ pages.set(page.slug, page);
111
+ }
112
+
113
+ const sortedPages = [...pages.values()];
114
+ // oxlint-disable-next-line eslint-plugin-unicorn/no-array-sort
115
+ sortedPages.sort((left, right) => left.slug.localeCompare(right.slug));
116
+
117
+ return {
118
+ description: config.description,
119
+ name: config.name,
120
+ pages: sortedPages,
121
+ segments: collectUtilitySegments(config, pages),
122
+ slug: config.slug,
123
+ };
124
+ };
125
+
126
+ const toUtilityDocPath = (value: string) => {
127
+ const clean = normalizePath(value);
128
+ if (!clean || clean === "index") {
129
+ return "/";
130
+ }
131
+ return `/${clean}`;
132
+ };
133
+
134
+ const toUtilityTemplatedDocUrl = (value: string) =>
135
+ `${UTILITY_DOCS_ROOT_TOKEN}${toUtilityDocPath(value)}`;
136
+
137
+ const toUtilityTemplatedMarkdownDocUrl = (value: string) => {
138
+ const clean = normalizePath(value);
139
+ if (!clean || clean === "index") {
140
+ return `${UTILITY_DOCS_ROOT_TOKEN}/index.md`;
141
+ }
142
+ return `${UTILITY_DOCS_ROOT_TOKEN}/${clean}.md`;
143
+ };
144
+
145
+ export const getPrebuiltUtilityLlmPagePath = (slug: string) => {
146
+ const normalized = normalizePath(slug);
147
+ return `_utility/llms-pages/${normalized || "index"}.mdx`;
148
+ };
149
+
150
+ export const buildUtilityArtifacts = (
151
+ index: UtilityIndex
152
+ ): UtilityArtifact[] => {
153
+ const segments = index.segments ?? [];
154
+ const segmentLines =
155
+ segments.length > 0
156
+ ? [
157
+ "",
158
+ "## Segments",
159
+ ...segments.map(
160
+ (segment) =>
161
+ `- [${segment.title}](${UTILITY_DOCS_ROOT_TOKEN}/llms/${segment.slug}.txt)`
162
+ ),
163
+ ]
164
+ : [];
165
+ const llmsLines = [
166
+ `# ${index.name}`,
167
+ index.description ? `> ${index.description}` : null,
168
+ "",
169
+ `Sitemap: ${toUtilityTemplatedDocUrl("sitemap.xml")}`,
170
+ `Full content: ${UTILITY_DOCS_ROOT_TOKEN}/llms-full.txt`,
171
+ `Skills: ${UTILITY_DOCS_ROOT_TOKEN}/.well-known/skills/index.json`,
172
+ ...segmentLines,
173
+ "",
174
+ "## Docs",
175
+ ...index.pages.map((page) => {
176
+ const description = page.description ? `: ${page.description}` : "";
177
+ return `- [${page.title}](${toUtilityTemplatedMarkdownDocUrl(page.slug)})${description}`;
178
+ }),
179
+ ];
180
+
181
+ const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
182
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
183
+ ${index.pages
184
+ .map(
185
+ (page) => ` <url><loc>${toUtilityTemplatedDocUrl(page.slug)}</loc></url>`
186
+ )
187
+ .join("\n")}
188
+ </urlset>`;
189
+
190
+ const llmsFull = index.pages
191
+ .map((page) =>
192
+ formatMarkdownPageSection(
193
+ page.title,
194
+ toUtilityTemplatedDocUrl(page.slug),
195
+ prepareLlmsFullContent(page.content, UTILITY_DOCS_ROOT_TOKEN, ""),
196
+ page.description
197
+ )
198
+ )
199
+ .join("\n\n");
200
+
201
+ const skillSlug = index.slug ?? slugify(index.name);
202
+ const skillDescription =
203
+ `${index.name} documentation. ${index.description ?? ""}`.trim();
204
+ const skillsIndex = JSON.stringify(
205
+ {
206
+ skills: [
207
+ {
208
+ description: skillDescription,
209
+ files: ["SKILL.md"],
210
+ name: skillSlug,
211
+ },
212
+ ],
213
+ },
214
+ null,
215
+ 2
216
+ );
217
+
218
+ const topPages = index.pages.slice(0, 20);
219
+ const skillMdLines = [
220
+ "---",
221
+ `name: ${skillSlug}`,
222
+ `description: ${skillDescription} Use when working with ${index.name}, answering questions about its features, or helping users follow its guides.`,
223
+ "---",
224
+ "",
225
+ `# ${index.name}`,
226
+ "",
227
+ index.description ? `${index.description}\n` : "",
228
+ "## Documentation",
229
+ "",
230
+ `- Full docs index: ${UTILITY_DOCS_ROOT_TOKEN}/llms.txt`,
231
+ `- Complete docs content: ${UTILITY_DOCS_ROOT_TOKEN}/llms-full.txt`,
232
+ "- Append `.md` to any page URL for raw markdown",
233
+ "",
234
+ "## Key Pages",
235
+ "",
236
+ ...topPages.map((page) => {
237
+ const desc = page.description ? ` - ${page.description}` : "";
238
+ return `- [${page.title}](${toUtilityTemplatedMarkdownDocUrl(
239
+ page.slug
240
+ )})${desc}`;
241
+ }),
242
+ ];
243
+
244
+ return [
245
+ {
246
+ content: sitemap,
247
+ contentType: "application/xml; charset=utf-8",
248
+ path: PREBUILT_UTILITY_SITEMAP_PATH,
249
+ },
250
+ {
251
+ content: llmsLines.filter((line) => line !== null).join("\n"),
252
+ contentType: "text/plain; charset=utf-8",
253
+ path: PREBUILT_UTILITY_LLMS_PATH,
254
+ },
255
+ {
256
+ content: llmsFull,
257
+ contentType: "text/plain; charset=utf-8",
258
+ path: PREBUILT_UTILITY_LLMS_FULL_PATH,
259
+ },
260
+ {
261
+ content: skillsIndex,
262
+ contentType: "application/json; charset=utf-8",
263
+ path: PREBUILT_UTILITY_SKILLS_INDEX_PATH,
264
+ },
265
+ {
266
+ content: skillMdLines.filter((line) => line !== null).join("\n"),
267
+ contentType: "text/markdown; charset=utf-8",
268
+ path: `${PREBUILT_UTILITY_SKILLS_MD_PREFIX}${skillSlug}/SKILL.md`,
269
+ },
270
+ ...segments.map((segment) => {
271
+ const segmentPages = index.pages.filter((page) =>
272
+ segment.pageSlugs.includes(page.slug)
273
+ );
274
+ const content = [
275
+ `# ${index.name} - ${segment.title}`,
276
+ `> Segment of ${UTILITY_DOCS_ROOT_TOKEN}/llms-full.txt`,
277
+ "",
278
+ ...segmentPages.map((page) =>
279
+ formatMarkdownPageSection(
280
+ page.title,
281
+ toUtilityTemplatedDocUrl(page.slug),
282
+ prepareLlmsFullContent(page.content, UTILITY_DOCS_ROOT_TOKEN, ""),
283
+ page.description
284
+ )
285
+ ),
286
+ ].join("\n\n");
287
+
288
+ return {
289
+ content,
290
+ contentType: "text/plain; charset=utf-8",
291
+ path: `${PREBUILT_UTILITY_LLMS_SEGMENT_PREFIX}${segment.slug}.txt`,
292
+ };
293
+ }),
294
+ ...index.pages.map((page) => ({
295
+ content: formatMarkdownPage(
296
+ page.title,
297
+ toAgentMarkdown(page.content),
298
+ page.description
299
+ ),
300
+ contentType: "text/markdown; charset=utf-8",
301
+ path: getPrebuiltUtilityLlmPagePath(page.slug),
302
+ })),
303
+ ];
304
+ };