blodemd 0.0.7 → 0.0.9
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/README.md +25 -9
- package/dev-server/app/[[...slug]]/page.tsx +1 -0
- package/dev-server/next.config.js +11 -13
- package/dev-server/package.json +1 -1
- package/dev-server/tsconfig.json +3 -0
- package/dist/cli.mjs +869 -184
- package/dist/cli.mjs.map +1 -1
- package/docs/components/api/api-playground.tsx +255 -80
- package/docs/components/api/api-reference.tsx +11 -1
- package/docs/components/docs/contextual-menu.tsx +227 -142
- package/docs/components/docs/copy-page-menu.tsx +132 -85
- package/docs/components/docs/doc-header.tsx +13 -3
- package/docs/components/docs/doc-shell.tsx +22 -11
- package/docs/components/docs/mobile-nav.tsx +0 -6
- package/docs/components/mdx/code-group.tsx +171 -62
- package/docs/components/mdx/tabs.tsx +131 -26
- package/docs/components/ui/input.tsx +0 -1
- package/docs/components/ui/search.tsx +241 -132
- package/docs/lib/content-root.ts +33 -0
- package/docs/lib/content-source.ts +70 -0
- package/docs/lib/contextual-options.ts +20 -0
- package/docs/lib/docs-runtime.tsx +595 -0
- package/docs/lib/edge-config.ts +95 -0
- package/docs/lib/env.ts +22 -0
- package/docs/lib/openapi-proxy.ts +88 -0
- package/docs/lib/platform-config.ts +6 -0
- package/docs/lib/routes.ts +39 -0
- package/docs/lib/supabase.ts +13 -0
- package/docs/lib/tenancy.ts +322 -0
- package/docs/lib/tenant-headers.ts +14 -0
- package/docs/lib/tenant-static.ts +529 -0
- package/docs/lib/tenant-utility-context.ts +62 -0
- package/docs/lib/tenants.ts +68 -0
- package/docs/lib/use-mobile.ts +19 -0
- package/package.json +3 -2
- package/packages/@repo/common/dist/index.d.ts +7 -0
- package/packages/@repo/common/dist/index.d.ts.map +1 -1
- package/packages/@repo/common/dist/index.js +42 -0
- package/packages/@repo/common/src/index.ts +50 -0
- package/packages/@repo/contracts/dist/project.d.ts +1 -1
- package/packages/@repo/contracts/dist/project.js +1 -1
- package/packages/@repo/contracts/src/project.ts +1 -1
- package/packages/@repo/models/dist/docs-config.d.ts +194 -29
- package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
- package/packages/@repo/models/dist/docs-config.js +3 -28
- package/packages/@repo/models/src/docs-config.ts +5 -31
- package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/blob-source.js +7 -2
- package/packages/@repo/previewing/dist/fs-source.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/fs-source.js +2 -3
- package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/index.js +1 -41
- package/packages/@repo/previewing/src/blob-source.ts +7 -4
- package/packages/@repo/previewing/src/fs-source.ts +2 -3
- package/packages/@repo/previewing/src/index.ts +3 -55
- package/packages/@repo/validation/dist/index.d.ts +2 -2
- package/packages/@repo/validation/dist/index.d.ts.map +1 -1
- package/packages/@repo/validation/dist/index.js +2 -2
- package/packages/@repo/validation/package.json +1 -0
- package/packages/@repo/validation/src/{mintlify-docs-schema.json → blodemd-docs-schema.json} +346 -1794
- package/packages/@repo/validation/src/index.ts +4 -4
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import { slugify } from "@repo/common";
|
|
2
|
+
import type { TenantResolution } from "@repo/contracts";
|
|
3
|
+
import type { Tenant } from "@repo/models";
|
|
4
|
+
import type { UtilityIndex } from "@repo/previewing";
|
|
5
|
+
import {
|
|
6
|
+
buildContentIndex,
|
|
7
|
+
buildPageMetadataMap,
|
|
8
|
+
getPrebuiltUtilityLlmPagePath,
|
|
9
|
+
loadContentSource,
|
|
10
|
+
loadPrebuiltContentIndex,
|
|
11
|
+
loadPrebuiltUtilityIndex,
|
|
12
|
+
loadSiteConfig,
|
|
13
|
+
PREBUILT_UTILITY_LLMS_FULL_PATH,
|
|
14
|
+
PREBUILT_UTILITY_LLMS_PATH,
|
|
15
|
+
PREBUILT_UTILITY_SITEMAP_PATH,
|
|
16
|
+
UTILITY_DOCS_ROOT_TOKEN,
|
|
17
|
+
} from "@repo/previewing";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
getTenantContentSource,
|
|
21
|
+
resolveSiteConfigAssets,
|
|
22
|
+
} from "@/lib/content-source";
|
|
23
|
+
import {
|
|
24
|
+
getDocsCollection,
|
|
25
|
+
getDocsCollectionWithNavigation,
|
|
26
|
+
getDocsNavigation,
|
|
27
|
+
} from "@/lib/docs-collection";
|
|
28
|
+
import {
|
|
29
|
+
buildNavigation,
|
|
30
|
+
enrichNavWithMetadata,
|
|
31
|
+
flattenNav,
|
|
32
|
+
getVisibleNavigation,
|
|
33
|
+
} from "@/lib/navigation";
|
|
34
|
+
import type { OpenApiEntry } from "@/lib/openapi";
|
|
35
|
+
import { loadOpenApiRegistry } from "@/lib/openapi";
|
|
36
|
+
import { toDocHref } from "@/lib/routes";
|
|
37
|
+
import { createTimedPromiseCache } from "@/lib/server-cache";
|
|
38
|
+
import { getRequestProtocol } from "@/lib/tenancy";
|
|
39
|
+
import type { TenantRequestContext } from "@/lib/tenant-utility-context";
|
|
40
|
+
|
|
41
|
+
const TENANT_STATIC_CACHE_TTL_MS = 30 * 60 * 1000;
|
|
42
|
+
|
|
43
|
+
const getTenantStaticCacheKey = (tenant: Tenant) =>
|
|
44
|
+
[
|
|
45
|
+
tenant.id,
|
|
46
|
+
tenant.slug,
|
|
47
|
+
tenant.activeDeploymentId ?? "",
|
|
48
|
+
tenant.activeDeploymentManifestUrl ?? "",
|
|
49
|
+
tenant.docsPath ?? "",
|
|
50
|
+
tenant.pathPrefix ?? "",
|
|
51
|
+
tenant.primaryDomain,
|
|
52
|
+
].join(":");
|
|
53
|
+
|
|
54
|
+
const tenantUrlDataCache = createTimedPromiseCache<
|
|
55
|
+
string,
|
|
56
|
+
Awaited<ReturnType<typeof buildTenantUrlData>>
|
|
57
|
+
>({
|
|
58
|
+
maxEntries: 512,
|
|
59
|
+
ttlMs: TENANT_STATIC_CACHE_TTL_MS,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const tenantUtilityIndexCache = createTimedPromiseCache<string, UtilityIndex>({
|
|
63
|
+
maxEntries: 512,
|
|
64
|
+
ttlMs: TENANT_STATIC_CACHE_TTL_MS,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const isMissingContentFileError = (error: unknown) => {
|
|
68
|
+
if (
|
|
69
|
+
typeof error === "object" &&
|
|
70
|
+
error !== null &&
|
|
71
|
+
"code" in error &&
|
|
72
|
+
(error as { code?: string }).code === "ENOENT"
|
|
73
|
+
) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return error instanceof Error && error.message.includes("not found");
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const getCanonicalHost = (
|
|
81
|
+
tenant: Tenant,
|
|
82
|
+
requestedHost?: string,
|
|
83
|
+
strategy?: TenantResolution["strategy"] | null
|
|
84
|
+
) =>
|
|
85
|
+
(strategy === "custom-domain" || strategy === "path") && requestedHost
|
|
86
|
+
? requestedHost
|
|
87
|
+
: tenant.primaryDomain;
|
|
88
|
+
|
|
89
|
+
const getCanonicalBasePath = (
|
|
90
|
+
tenant: Tenant,
|
|
91
|
+
basePath?: string,
|
|
92
|
+
strategy?: TenantResolution["strategy"] | null
|
|
93
|
+
) => {
|
|
94
|
+
if (strategy === "path") {
|
|
95
|
+
return basePath || `/${tenant.slug}`;
|
|
96
|
+
}
|
|
97
|
+
if (strategy === "custom-domain") {
|
|
98
|
+
return basePath ?? tenant.pathPrefix ?? "";
|
|
99
|
+
}
|
|
100
|
+
return basePath ?? "";
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const getTenantRequestContextFromHeaders = (
|
|
104
|
+
tenant: Tenant,
|
|
105
|
+
headerStore: Pick<Headers, "get">
|
|
106
|
+
): TenantRequestContext => ({
|
|
107
|
+
basePath: headerStore.get("x-tenant-base-path") ?? tenant.pathPrefix ?? "",
|
|
108
|
+
protocol: getRequestProtocol(headerStore),
|
|
109
|
+
requestedHost: headerStore.get("x-tenant-domain") ?? undefined,
|
|
110
|
+
strategy: (headerStore.get("x-tenant-strategy") ?? null) as
|
|
111
|
+
| TenantResolution["strategy"]
|
|
112
|
+
| null,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export const getStaticTenantRequestContext = (
|
|
116
|
+
tenant: Tenant
|
|
117
|
+
): TenantRequestContext => ({
|
|
118
|
+
basePath: tenant.pathPrefix ?? "",
|
|
119
|
+
protocol: "https",
|
|
120
|
+
requestedHost: tenant.primaryDomain,
|
|
121
|
+
strategy: tenant.customDomains.length > 0 ? "custom-domain" : null,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
export const getCanonicalOrigin = (
|
|
125
|
+
tenant: Tenant,
|
|
126
|
+
context: TenantRequestContext = {}
|
|
127
|
+
) =>
|
|
128
|
+
`${context.protocol ?? "https"}://${getCanonicalHost(
|
|
129
|
+
tenant,
|
|
130
|
+
context.requestedHost,
|
|
131
|
+
context.strategy
|
|
132
|
+
)}`;
|
|
133
|
+
|
|
134
|
+
export const getCanonicalDocBasePath = (
|
|
135
|
+
tenant: Tenant,
|
|
136
|
+
context: TenantRequestContext = {}
|
|
137
|
+
) => getCanonicalBasePath(tenant, context.basePath, context.strategy);
|
|
138
|
+
|
|
139
|
+
const renderUtilityTemplate = (
|
|
140
|
+
source: string,
|
|
141
|
+
tenant: Tenant,
|
|
142
|
+
context: TenantRequestContext = {}
|
|
143
|
+
) =>
|
|
144
|
+
source.replaceAll(
|
|
145
|
+
UTILITY_DOCS_ROOT_TOKEN,
|
|
146
|
+
`${getCanonicalOrigin(tenant, context)}${getCanonicalDocBasePath(
|
|
147
|
+
tenant,
|
|
148
|
+
context
|
|
149
|
+
)}`
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const buildTenantUrlData = async (tenant: Tenant) => {
|
|
153
|
+
const contentSource = getTenantContentSource(tenant);
|
|
154
|
+
const configResult = await loadSiteConfig(contentSource);
|
|
155
|
+
if (!configResult.ok) {
|
|
156
|
+
throw new Error("Invalid config");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const config = await resolveSiteConfigAssets(
|
|
160
|
+
configResult.config,
|
|
161
|
+
contentSource
|
|
162
|
+
);
|
|
163
|
+
const contentIndex =
|
|
164
|
+
(await loadPrebuiltContentIndex(contentSource)) ??
|
|
165
|
+
(await buildContentIndex(contentSource, config));
|
|
166
|
+
if (contentIndex.errors.length) {
|
|
167
|
+
throw new Error("Invalid content");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const docsCollection = getDocsCollection(config);
|
|
171
|
+
const docsNavigation = getDocsNavigation(config);
|
|
172
|
+
const docsCollectionWithNavigation = getDocsCollectionWithNavigation(config);
|
|
173
|
+
const registry = await loadOpenApiRegistry(
|
|
174
|
+
docsCollectionWithNavigation,
|
|
175
|
+
contentSource
|
|
176
|
+
);
|
|
177
|
+
const metadataMap = buildPageMetadataMap(contentIndex);
|
|
178
|
+
const rawNav = docsNavigation
|
|
179
|
+
? buildNavigation(
|
|
180
|
+
docsNavigation,
|
|
181
|
+
registry,
|
|
182
|
+
docsCollection?.slugPrefix ?? ""
|
|
183
|
+
)
|
|
184
|
+
: [];
|
|
185
|
+
const nav = enrichNavWithMetadata(rawNav, metadataMap);
|
|
186
|
+
const visibleNav = getVisibleNavigation(nav);
|
|
187
|
+
const hiddenSlugs = new Set<string>();
|
|
188
|
+
for (const [slug, meta] of metadataMap) {
|
|
189
|
+
if (meta.hidden || meta.noindex) {
|
|
190
|
+
hiddenSlugs.add(slug);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const navPages = flattenNav(visibleNav).map((page) => page.path);
|
|
195
|
+
const contentPages = contentIndex.entries
|
|
196
|
+
.filter(
|
|
197
|
+
(entry) =>
|
|
198
|
+
!(entry.kind === "entry" && entry.hidden) &&
|
|
199
|
+
!hiddenSlugs.has(entry.slug)
|
|
200
|
+
)
|
|
201
|
+
.map((entry) => entry.slug);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
config,
|
|
205
|
+
contentIndex,
|
|
206
|
+
contentSource,
|
|
207
|
+
hiddenSlugs,
|
|
208
|
+
pages: [...new Set([...navPages, ...contentPages])],
|
|
209
|
+
registry,
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const loadTenantUrlData = async (tenant: Tenant) =>
|
|
214
|
+
await tenantUrlDataCache.getOrCreate(
|
|
215
|
+
getTenantStaticCacheKey(tenant),
|
|
216
|
+
async () => await buildTenantUrlData(tenant)
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const formatOpenApiPageText = (entry: OpenApiEntry): string => {
|
|
220
|
+
const parts = [
|
|
221
|
+
`Method: ${entry.operation.method}`,
|
|
222
|
+
`Path: ${entry.operation.path}`,
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
if (entry.operation.description) {
|
|
226
|
+
parts.push(entry.operation.description);
|
|
227
|
+
}
|
|
228
|
+
if (entry.operation.tags.length) {
|
|
229
|
+
parts.push(`Tags: ${entry.operation.tags.join(", ")}`);
|
|
230
|
+
}
|
|
231
|
+
if (entry.operation.parameters.length) {
|
|
232
|
+
parts.push(
|
|
233
|
+
`Parameters:\n${JSON.stringify(entry.operation.parameters, null, 2)}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (entry.operation.requestBody) {
|
|
237
|
+
parts.push(
|
|
238
|
+
`Request Body:\n${JSON.stringify(entry.operation.requestBody, null, 2)}`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (entry.operation.responses) {
|
|
242
|
+
parts.push(
|
|
243
|
+
`Responses:\n${JSON.stringify(entry.operation.responses, null, 2)}`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return parts.join("\n\n");
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const resolveLlmPages = (
|
|
251
|
+
data: Awaited<ReturnType<typeof loadTenantUrlData>>
|
|
252
|
+
): (
|
|
253
|
+
| {
|
|
254
|
+
kind: "content";
|
|
255
|
+
relativePath: string;
|
|
256
|
+
slug: string;
|
|
257
|
+
title: string;
|
|
258
|
+
description?: string;
|
|
259
|
+
}
|
|
260
|
+
| {
|
|
261
|
+
kind: "openapi";
|
|
262
|
+
entry: OpenApiEntry;
|
|
263
|
+
slug: string;
|
|
264
|
+
title: string;
|
|
265
|
+
description?: string;
|
|
266
|
+
}
|
|
267
|
+
)[] =>
|
|
268
|
+
data.pages
|
|
269
|
+
.map((slug) => {
|
|
270
|
+
const contentEntry = data.contentIndex.bySlug.get(slug);
|
|
271
|
+
if (
|
|
272
|
+
contentEntry &&
|
|
273
|
+
contentEntry.kind === "entry" &&
|
|
274
|
+
!contentEntry.hidden &&
|
|
275
|
+
!data.hiddenSlugs.has(contentEntry.slug)
|
|
276
|
+
) {
|
|
277
|
+
return {
|
|
278
|
+
description: contentEntry.description,
|
|
279
|
+
kind: "content" as const,
|
|
280
|
+
relativePath: contentEntry.relativePath,
|
|
281
|
+
slug: contentEntry.slug,
|
|
282
|
+
title: contentEntry.title,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const openApiEntry = data.registry.bySlug.get(slug);
|
|
287
|
+
if (!openApiEntry) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
description: openApiEntry.operation.description,
|
|
293
|
+
entry: openApiEntry,
|
|
294
|
+
kind: "openapi" as const,
|
|
295
|
+
slug,
|
|
296
|
+
title: openApiEntry.operation.summary ?? openApiEntry.identifier,
|
|
297
|
+
};
|
|
298
|
+
})
|
|
299
|
+
.filter((page) => page !== null);
|
|
300
|
+
|
|
301
|
+
const FRONTMATTER_REGEX = /^---\s*\n[\s\S]*?\n---\s*\n?/;
|
|
302
|
+
const LEADING_H1_REGEX = /^#\s+([^\r\n]+)(?:\r?\n(?:\r?\n)?)?/;
|
|
303
|
+
|
|
304
|
+
const stripFrontmatter = (source: string) =>
|
|
305
|
+
source.replace(FRONTMATTER_REGEX, "").trim();
|
|
306
|
+
|
|
307
|
+
const stripMatchingLeadingH1 = (source: string, title: string) => {
|
|
308
|
+
const trimmed = source.trimStart();
|
|
309
|
+
const match = LEADING_H1_REGEX.exec(trimmed);
|
|
310
|
+
if (!match) {
|
|
311
|
+
return trimmed.trim();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const [headingLine = "", headingTitle = ""] = match;
|
|
315
|
+
if (slugify(headingTitle) !== slugify(title)) {
|
|
316
|
+
return trimmed.trim();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return trimmed.slice(headingLine.length).trim();
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const formatMarkdownPage = (title: string, source: string) => {
|
|
323
|
+
const content = stripMatchingLeadingH1(source, title);
|
|
324
|
+
if (!content) {
|
|
325
|
+
return `# ${title}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return `# ${title}\n\n${content}`;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const formatMarkdownPageSection = (
|
|
332
|
+
title: string,
|
|
333
|
+
url: string,
|
|
334
|
+
source: string
|
|
335
|
+
) => {
|
|
336
|
+
const content = stripMatchingLeadingH1(source, title);
|
|
337
|
+
if (!content) {
|
|
338
|
+
return `# ${title} (${url})`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return `# ${title} (${url})\n\n${content}`;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const buildRuntimeUtilityIndex = async (
|
|
345
|
+
tenant: Tenant
|
|
346
|
+
): Promise<UtilityIndex> => {
|
|
347
|
+
const data = await loadTenantUrlData(tenant);
|
|
348
|
+
const pages = await Promise.all(
|
|
349
|
+
resolveLlmPages(data).map(async (page) => {
|
|
350
|
+
if (page.kind === "openapi") {
|
|
351
|
+
return {
|
|
352
|
+
content: formatOpenApiPageText(page.entry),
|
|
353
|
+
description: page.description,
|
|
354
|
+
slug: page.slug,
|
|
355
|
+
title: page.title,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const raw = await loadContentSource(
|
|
360
|
+
data.contentSource,
|
|
361
|
+
page.relativePath
|
|
362
|
+
);
|
|
363
|
+
return {
|
|
364
|
+
content: stripFrontmatter(raw),
|
|
365
|
+
description: page.description,
|
|
366
|
+
slug: page.slug,
|
|
367
|
+
title: page.title,
|
|
368
|
+
};
|
|
369
|
+
})
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
description: data.config.description,
|
|
374
|
+
name: data.config.name,
|
|
375
|
+
pages,
|
|
376
|
+
};
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const loadTenantUtilityIndex = async (tenant: Tenant) =>
|
|
380
|
+
await tenantUtilityIndexCache.getOrCreate(
|
|
381
|
+
getTenantStaticCacheKey(tenant),
|
|
382
|
+
async () => {
|
|
383
|
+
const prebuilt = await loadPrebuiltUtilityIndex(
|
|
384
|
+
getTenantContentSource(tenant)
|
|
385
|
+
);
|
|
386
|
+
if (prebuilt) {
|
|
387
|
+
return prebuilt;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return await buildRuntimeUtilityIndex(tenant);
|
|
391
|
+
}
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const loadTenantUtilityTemplate = async (tenant: Tenant, path: string) => {
|
|
395
|
+
const contentSource = getTenantContentSource(tenant);
|
|
396
|
+
try {
|
|
397
|
+
return await contentSource.readFile(path);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (isMissingContentFileError(error)) {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
export const clearTenantStaticCaches = () => {
|
|
408
|
+
tenantUrlDataCache.clear();
|
|
409
|
+
tenantUtilityIndexCache.clear();
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
export const clearTenantStaticCachesForTenant = (tenantId: string) => {
|
|
413
|
+
tenantUrlDataCache.deleteByPrefix(tenantId);
|
|
414
|
+
tenantUtilityIndexCache.deleteByPrefix(tenantId);
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
export const buildTenantSitemapXml = async (
|
|
418
|
+
tenant: Tenant,
|
|
419
|
+
context: TenantRequestContext = {}
|
|
420
|
+
) => {
|
|
421
|
+
const prebuilt = await loadTenantUtilityTemplate(
|
|
422
|
+
tenant,
|
|
423
|
+
PREBUILT_UTILITY_SITEMAP_PATH
|
|
424
|
+
);
|
|
425
|
+
if (prebuilt) {
|
|
426
|
+
return renderUtilityTemplate(prebuilt, tenant, context);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const { pages } = await loadTenantUtilityIndex(tenant);
|
|
430
|
+
const origin = getCanonicalOrigin(tenant, context);
|
|
431
|
+
const basePath = getCanonicalDocBasePath(tenant, context);
|
|
432
|
+
const urls = pages.map(
|
|
433
|
+
(page) => `${origin}${toDocHref(page.slug, basePath)}`
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
437
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
438
|
+
${urls.map((url) => ` <url><loc>${url}</loc></url>`).join("\n")}
|
|
439
|
+
</urlset>`;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
export const buildTenantRobotsTxt = (
|
|
443
|
+
tenant: Tenant,
|
|
444
|
+
context: TenantRequestContext = {}
|
|
445
|
+
) => {
|
|
446
|
+
const origin = getCanonicalOrigin(tenant, context);
|
|
447
|
+
const basePath = getCanonicalDocBasePath(tenant, context);
|
|
448
|
+
return `User-agent: *
|
|
449
|
+
Allow: /
|
|
450
|
+
Sitemap: ${origin}${toDocHref("sitemap.xml", basePath)}
|
|
451
|
+
|
|
452
|
+
# LLM-friendly content
|
|
453
|
+
# ${origin}${toDocHref("llms.txt", basePath)} - Index of all documentation pages
|
|
454
|
+
# ${origin}${toDocHref("llms-full.txt", basePath)} - Full documentation content
|
|
455
|
+
# Append .md to any page URL for raw markdown`;
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
export const buildTenantLlmsTxt = async (
|
|
459
|
+
tenant: Tenant,
|
|
460
|
+
context: TenantRequestContext = {}
|
|
461
|
+
) => {
|
|
462
|
+
const prebuilt = await loadTenantUtilityTemplate(
|
|
463
|
+
tenant,
|
|
464
|
+
PREBUILT_UTILITY_LLMS_PATH
|
|
465
|
+
);
|
|
466
|
+
if (prebuilt) {
|
|
467
|
+
return renderUtilityTemplate(prebuilt, tenant, context);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const data = await loadTenantUtilityIndex(tenant);
|
|
471
|
+
const origin = getCanonicalOrigin(tenant, context);
|
|
472
|
+
const basePath = getCanonicalDocBasePath(tenant, context);
|
|
473
|
+
|
|
474
|
+
const lines = [
|
|
475
|
+
`# ${data.name}`,
|
|
476
|
+
data.description ? `> ${data.description}` : null,
|
|
477
|
+
"",
|
|
478
|
+
`Sitemap: ${origin}${toDocHref("sitemap.xml", basePath)}`,
|
|
479
|
+
"",
|
|
480
|
+
"## Docs",
|
|
481
|
+
...data.pages.map((page) => {
|
|
482
|
+
const url = `${origin}${toDocHref(page.slug, basePath)}`;
|
|
483
|
+
const desc = page.description ? `: ${page.description}` : "";
|
|
484
|
+
return `- [${page.title}](${url})${desc}`;
|
|
485
|
+
}),
|
|
486
|
+
];
|
|
487
|
+
|
|
488
|
+
return lines.filter((line) => line !== null).join("\n");
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
export const buildTenantLlmsFullTxt = async (
|
|
492
|
+
tenant: Tenant,
|
|
493
|
+
context: TenantRequestContext = {}
|
|
494
|
+
) => {
|
|
495
|
+
const prebuilt = await loadTenantUtilityTemplate(
|
|
496
|
+
tenant,
|
|
497
|
+
PREBUILT_UTILITY_LLMS_FULL_PATH
|
|
498
|
+
);
|
|
499
|
+
if (prebuilt) {
|
|
500
|
+
return renderUtilityTemplate(prebuilt, tenant, context);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const data = await loadTenantUtilityIndex(tenant);
|
|
504
|
+
const origin = getCanonicalOrigin(tenant, context);
|
|
505
|
+
const basePath = getCanonicalDocBasePath(tenant, context);
|
|
506
|
+
const parts = data.pages.map((page) => {
|
|
507
|
+
const url = `${origin}${toDocHref(page.slug, basePath)}`;
|
|
508
|
+
return formatMarkdownPageSection(page.title, url, page.content);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
return parts.join("\n\n");
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
export const getLlmPageText = async (tenant: Tenant, slug: string) => {
|
|
515
|
+
const prebuilt = await loadTenantUtilityTemplate(
|
|
516
|
+
tenant,
|
|
517
|
+
getPrebuiltUtilityLlmPagePath(slug)
|
|
518
|
+
);
|
|
519
|
+
if (prebuilt) {
|
|
520
|
+
return prebuilt;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const data = await loadTenantUtilityIndex(tenant);
|
|
524
|
+
const page = data.pages.find((item) => item.slug === slug);
|
|
525
|
+
if (!page) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
return formatMarkdownPage(page.title, page.content);
|
|
529
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { TenantResolution } from "@repo/contracts";
|
|
2
|
+
|
|
3
|
+
export interface TenantRequestContext {
|
|
4
|
+
basePath?: string;
|
|
5
|
+
protocol?: string;
|
|
6
|
+
requestedHost?: string;
|
|
7
|
+
strategy?: TenantResolution["strategy"] | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const TENANT_UTILITY_CONTEXT_PARAMS = {
|
|
11
|
+
basePath: "__blodemd_base_path",
|
|
12
|
+
host: "__blodemd_host",
|
|
13
|
+
protocol: "__blodemd_protocol",
|
|
14
|
+
strategy: "__blodemd_strategy",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export const applyTenantUtilityContextSearchParams = (
|
|
18
|
+
url: URL,
|
|
19
|
+
context: TenantRequestContext
|
|
20
|
+
) => {
|
|
21
|
+
if (context.requestedHost) {
|
|
22
|
+
url.searchParams.set(
|
|
23
|
+
TENANT_UTILITY_CONTEXT_PARAMS.host,
|
|
24
|
+
context.requestedHost
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
url.searchParams.set(
|
|
29
|
+
TENANT_UTILITY_CONTEXT_PARAMS.basePath,
|
|
30
|
+
context.basePath ?? ""
|
|
31
|
+
);
|
|
32
|
+
url.searchParams.set(
|
|
33
|
+
TENANT_UTILITY_CONTEXT_PARAMS.protocol,
|
|
34
|
+
context.protocol ?? "https"
|
|
35
|
+
);
|
|
36
|
+
url.searchParams.set(
|
|
37
|
+
TENANT_UTILITY_CONTEXT_PARAMS.strategy,
|
|
38
|
+
context.strategy ?? ""
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const getTenantRequestContextFromUrl = (
|
|
43
|
+
url: URL
|
|
44
|
+
): TenantRequestContext | null => {
|
|
45
|
+
const requestedHost = url.searchParams.get(
|
|
46
|
+
TENANT_UTILITY_CONTEXT_PARAMS.host
|
|
47
|
+
);
|
|
48
|
+
const basePath = url.searchParams.get(TENANT_UTILITY_CONTEXT_PARAMS.basePath);
|
|
49
|
+
const protocol = url.searchParams.get(TENANT_UTILITY_CONTEXT_PARAMS.protocol);
|
|
50
|
+
const strategy = url.searchParams.get(TENANT_UTILITY_CONTEXT_PARAMS.strategy);
|
|
51
|
+
|
|
52
|
+
if (!(requestedHost || basePath !== null || protocol || strategy)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
basePath: basePath ?? undefined,
|
|
58
|
+
protocol: protocol || undefined,
|
|
59
|
+
requestedHost: requestedHost || undefined,
|
|
60
|
+
strategy: (strategy || null) as TenantRequestContext["strategy"],
|
|
61
|
+
};
|
|
62
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { TenantSchema } from "@repo/contracts";
|
|
2
|
+
import type { Tenant } from "@repo/models";
|
|
3
|
+
|
|
4
|
+
import { getTenantDocsPath } from "./content-root";
|
|
5
|
+
import { getTenantEdgeSlugRecord, isEdgeConfigEnabled } from "./edge-config";
|
|
6
|
+
import { docsApiBase } from "./env";
|
|
7
|
+
import { createTimedPromiseCache } from "./server-cache";
|
|
8
|
+
|
|
9
|
+
export const getProjectTag = (slug: string) => `project:${slug}`;
|
|
10
|
+
|
|
11
|
+
const TENANT_REVALIDATE_SECONDS = 3600;
|
|
12
|
+
|
|
13
|
+
const mapTenant = (tenant: {
|
|
14
|
+
activeDeploymentId?: string;
|
|
15
|
+
activeDeploymentManifestUrl?: string;
|
|
16
|
+
id: string;
|
|
17
|
+
slug: string;
|
|
18
|
+
name: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
primaryDomain: string;
|
|
21
|
+
subdomain: string;
|
|
22
|
+
customDomains: string[];
|
|
23
|
+
pathPrefix?: string;
|
|
24
|
+
status: "active" | "disabled";
|
|
25
|
+
}): Tenant => ({
|
|
26
|
+
...tenant,
|
|
27
|
+
docsPath: getTenantDocsPath(tenant.slug),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const tenantCache = createTimedPromiseCache<string, Tenant | null>({
|
|
31
|
+
maxEntries: 512,
|
|
32
|
+
ttlMs: TENANT_REVALIDATE_SECONDS * 1000,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const fetchTenantFromApi = async (slug: string): Promise<Tenant | null> => {
|
|
36
|
+
const url = new URL(`/tenants/${slug}`, docsApiBase);
|
|
37
|
+
const response = await fetch(url.toString(), {
|
|
38
|
+
next: {
|
|
39
|
+
revalidate: TENANT_REVALIDATE_SECONDS,
|
|
40
|
+
tags: [getProjectTag(slug), "tenants"],
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const json = (await response.json()) as unknown;
|
|
47
|
+
const parsed = TenantSchema.safeParse(json);
|
|
48
|
+
if (!parsed.success) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return mapTenant(parsed.data);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const fetchTenant = async (slug: string): Promise<Tenant | null> => {
|
|
55
|
+
if (isEdgeConfigEnabled()) {
|
|
56
|
+
const edgeRecord = await getTenantEdgeSlugRecord(slug);
|
|
57
|
+
return edgeRecord ? mapTenant(edgeRecord.tenant) : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return await fetchTenantFromApi(slug);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const clearTenantCache = () => {
|
|
64
|
+
tenantCache.clear();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const getTenantBySlug = async (slug: string) =>
|
|
68
|
+
await tenantCache.getOrCreate(slug, async () => await fetchTenant(slug));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768;
|
|
4
|
+
|
|
5
|
+
export const useIsMobile = () => {
|
|
6
|
+
const [isMobile, setIsMobile] = useState<boolean | undefined>();
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
10
|
+
const onChange = () => {
|
|
11
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
12
|
+
};
|
|
13
|
+
mql.addEventListener("change", onChange);
|
|
14
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
15
|
+
return () => mql.removeEventListener("change", onChange);
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
return !!isMobile;
|
|
19
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blodemd",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Blode.md CLI",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"@base-ui/react": "^1.3.0",
|
|
41
41
|
"@clack/prompts": "^1.0.0",
|
|
42
42
|
"@mdx-js/mdx": "^3.1.1",
|
|
43
|
+
"@repo/common": "*",
|
|
43
44
|
"@shikijs/rehype": "^4.0.2",
|
|
44
45
|
"@tailwindcss/postcss": "^4.2.2",
|
|
45
46
|
"@types/node": "^22.19.15",
|
|
@@ -81,6 +82,6 @@
|
|
|
81
82
|
"ultracite": "^7.3.2"
|
|
82
83
|
},
|
|
83
84
|
"engines": {
|
|
84
|
-
"node": ">=20.17.0
|
|
85
|
+
"node": ">=20.17.0"
|
|
85
86
|
}
|
|
86
87
|
}
|