blodemd 0.0.8 → 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,595 @@
|
|
|
1
|
+
import { normalizePath } from "@repo/common";
|
|
2
|
+
import type { PageMode, Tenant } from "@repo/models";
|
|
3
|
+
import {
|
|
4
|
+
buildContentIndex,
|
|
5
|
+
buildPageMetadataMap,
|
|
6
|
+
buildSearchIndex,
|
|
7
|
+
buildUtilityIndex,
|
|
8
|
+
loadContentSource,
|
|
9
|
+
loadPrebuiltContentIndex,
|
|
10
|
+
loadPrebuiltSearchIndex,
|
|
11
|
+
loadPrebuiltTocIndex,
|
|
12
|
+
loadPrebuiltUtilityIndex,
|
|
13
|
+
loadSiteConfig,
|
|
14
|
+
} from "@repo/previewing";
|
|
15
|
+
import type {
|
|
16
|
+
ContentIndex,
|
|
17
|
+
ContentSource,
|
|
18
|
+
PageMetadata,
|
|
19
|
+
SearchIndexItem,
|
|
20
|
+
TocItem,
|
|
21
|
+
UtilityIndex,
|
|
22
|
+
} from "@repo/previewing";
|
|
23
|
+
import { cache } from "react";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
getTenantContentSource,
|
|
27
|
+
resolveSiteConfigAssets,
|
|
28
|
+
} from "@/lib/content-source";
|
|
29
|
+
import { contextualOptionsRequirePageContent } from "@/lib/contextual-options";
|
|
30
|
+
import {
|
|
31
|
+
getDocsCollection,
|
|
32
|
+
getDocsCollectionWithNavigation,
|
|
33
|
+
getDocsNavigation,
|
|
34
|
+
} from "@/lib/docs-collection";
|
|
35
|
+
import { renderFromCompiled, renderMdx } from "@/lib/mdx";
|
|
36
|
+
import {
|
|
37
|
+
buildNavigation,
|
|
38
|
+
buildTabbedNavigation,
|
|
39
|
+
enrichNavWithMetadata,
|
|
40
|
+
findActiveTabIndex,
|
|
41
|
+
findBreadcrumbs,
|
|
42
|
+
flattenNav,
|
|
43
|
+
getVisibleNavigation,
|
|
44
|
+
} from "@/lib/navigation";
|
|
45
|
+
import type { NavEntry, NavPage, NavTab } from "@/lib/navigation";
|
|
46
|
+
import { loadOpenApiRegistry } from "@/lib/openapi";
|
|
47
|
+
import type { OpenApiRegistry } from "@/lib/openapi";
|
|
48
|
+
import { createTimedPromiseCache } from "@/lib/server-cache";
|
|
49
|
+
import { getTenantBySlug } from "@/lib/tenants";
|
|
50
|
+
import { extractToc } from "@/lib/toc";
|
|
51
|
+
|
|
52
|
+
interface ConfigErrorResult {
|
|
53
|
+
configErrors: string[];
|
|
54
|
+
configWarnings: string[];
|
|
55
|
+
tenant: Tenant;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface UnpublishedTenantResult {
|
|
59
|
+
emptyState: "unpublished";
|
|
60
|
+
tenant: Tenant;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface RenderedPageData {
|
|
64
|
+
content: Awaited<ReturnType<typeof renderMdx>>["content"];
|
|
65
|
+
frontmatter: Awaited<ReturnType<typeof renderMdx>>["frontmatter"];
|
|
66
|
+
rawContent?: string;
|
|
67
|
+
toc: ReturnType<typeof extractToc>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface TenantArtifacts {
|
|
71
|
+
anchors: { label: string; href: string }[];
|
|
72
|
+
config: Awaited<ReturnType<typeof resolveSiteConfigAssets>>;
|
|
73
|
+
contentIndex: ContentIndex;
|
|
74
|
+
contentSource: ContentSource;
|
|
75
|
+
flatNav: NavPage[];
|
|
76
|
+
pageMetadataMap: Map<string, PageMetadata>;
|
|
77
|
+
registry: OpenApiRegistry;
|
|
78
|
+
tabs: NavTab[] | null;
|
|
79
|
+
tenant: Tenant;
|
|
80
|
+
tocBySlug: Map<string, TocItem[]>;
|
|
81
|
+
visibleFlatNav: NavPage[];
|
|
82
|
+
visibleNav: NavEntry[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type TenantArtifactsResult =
|
|
86
|
+
| ConfigErrorResult
|
|
87
|
+
| TenantArtifacts
|
|
88
|
+
| UnpublishedTenantResult
|
|
89
|
+
| null;
|
|
90
|
+
|
|
91
|
+
const ARTIFACT_CACHE_TTL_MS = 30 * 60 * 1000;
|
|
92
|
+
const PAGE_RENDER_CACHE_TTL_MS = 4 * 60 * 60 * 1000;
|
|
93
|
+
|
|
94
|
+
const artifactsCache = createTimedPromiseCache<string, TenantArtifactsResult>({
|
|
95
|
+
maxEntries: 512,
|
|
96
|
+
ttlMs: ARTIFACT_CACHE_TTL_MS,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const searchItemsCache = createTimedPromiseCache<string, SearchIndexItem[]>({
|
|
100
|
+
maxEntries: 512,
|
|
101
|
+
ttlMs: ARTIFACT_CACHE_TTL_MS,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const renderedPageCache = createTimedPromiseCache<string, RenderedPageData>({
|
|
105
|
+
maxEntries: 2048,
|
|
106
|
+
ttlMs: PAGE_RENDER_CACHE_TTL_MS,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const isConfigErrorResult = (
|
|
110
|
+
value: TenantArtifactsResult
|
|
111
|
+
): value is ConfigErrorResult => Boolean(value && "configErrors" in value);
|
|
112
|
+
|
|
113
|
+
const isUnpublishedTenantResult = (
|
|
114
|
+
value: TenantArtifactsResult
|
|
115
|
+
): value is UnpublishedTenantResult => Boolean(value && "emptyState" in value);
|
|
116
|
+
|
|
117
|
+
const getTenantArtifactsCacheKey = (tenant: Tenant) =>
|
|
118
|
+
[
|
|
119
|
+
tenant.id,
|
|
120
|
+
tenant.slug,
|
|
121
|
+
tenant.activeDeploymentId ?? "",
|
|
122
|
+
tenant.activeDeploymentManifestUrl ?? "",
|
|
123
|
+
tenant.docsPath ?? "",
|
|
124
|
+
tenant.pathPrefix ?? "",
|
|
125
|
+
tenant.primaryDomain,
|
|
126
|
+
].join(":");
|
|
127
|
+
|
|
128
|
+
const buildSearchItems = ({
|
|
129
|
+
config,
|
|
130
|
+
contentIndex,
|
|
131
|
+
utilityIndex,
|
|
132
|
+
}: {
|
|
133
|
+
config: TenantArtifacts["config"];
|
|
134
|
+
contentIndex: ContentIndex;
|
|
135
|
+
utilityIndex?: UtilityIndex | null;
|
|
136
|
+
}) => buildSearchIndex(contentIndex, config, utilityIndex ?? undefined);
|
|
137
|
+
|
|
138
|
+
const mergeSearchItems = ({
|
|
139
|
+
baseItems,
|
|
140
|
+
config,
|
|
141
|
+
flatNav,
|
|
142
|
+
visibleFlatNav,
|
|
143
|
+
}: {
|
|
144
|
+
baseItems: SearchIndexItem[];
|
|
145
|
+
config: TenantArtifacts["config"];
|
|
146
|
+
flatNav: NavPage[];
|
|
147
|
+
visibleFlatNav: NavPage[];
|
|
148
|
+
}): SearchIndexItem[] => {
|
|
149
|
+
const items = new Map<string, SearchIndexItem>(
|
|
150
|
+
baseItems.map((item) => [item.path, item])
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
for (const item of visibleFlatNav) {
|
|
154
|
+
items.set(item.path, {
|
|
155
|
+
href: item.url,
|
|
156
|
+
path: item.path,
|
|
157
|
+
title: item.sidebarTitle ?? item.title,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (config.seo?.indexing === "all") {
|
|
162
|
+
for (const item of flatNav) {
|
|
163
|
+
if (!items.has(item.path)) {
|
|
164
|
+
items.set(item.path, {
|
|
165
|
+
href: item.url,
|
|
166
|
+
path: item.path,
|
|
167
|
+
title: item.sidebarTitle ?? item.title,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return [...items.values()];
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const getTenantArtifacts = async (tenantSlug: string) => {
|
|
177
|
+
const tenant = await getTenantBySlug(tenantSlug);
|
|
178
|
+
if (!tenant) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const cacheKey = getTenantArtifactsCacheKey(tenant);
|
|
183
|
+
|
|
184
|
+
return await artifactsCache.getOrCreate(cacheKey, async () => {
|
|
185
|
+
const contentSource = getTenantContentSource(tenant);
|
|
186
|
+
const configResult = await loadSiteConfig(contentSource);
|
|
187
|
+
if (!configResult.ok) {
|
|
188
|
+
if (
|
|
189
|
+
!tenant.activeDeploymentManifestUrl &&
|
|
190
|
+
configResult.errors.length === 1 &&
|
|
191
|
+
configResult.errors[0] === "docs.json not found."
|
|
192
|
+
) {
|
|
193
|
+
return {
|
|
194
|
+
emptyState: "unpublished" as const,
|
|
195
|
+
tenant,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
configErrors: configResult.errors,
|
|
201
|
+
configWarnings: [],
|
|
202
|
+
tenant,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const rawConfig = configResult.config;
|
|
207
|
+
const docsCollection = getDocsCollection(rawConfig);
|
|
208
|
+
const docsNavigation = getDocsNavigation(rawConfig);
|
|
209
|
+
const docsCollectionWithNavigation =
|
|
210
|
+
getDocsCollectionWithNavigation(rawConfig);
|
|
211
|
+
|
|
212
|
+
const [config, contentIndex, registryResult, prebuiltTocIndex] =
|
|
213
|
+
await Promise.all([
|
|
214
|
+
resolveSiteConfigAssets(rawConfig, contentSource),
|
|
215
|
+
loadPrebuiltContentIndex(contentSource).then(
|
|
216
|
+
async (prebuilt) =>
|
|
217
|
+
prebuilt ?? (await buildContentIndex(contentSource, rawConfig))
|
|
218
|
+
),
|
|
219
|
+
loadOpenApiRegistry(docsCollectionWithNavigation, contentSource)
|
|
220
|
+
.then((registry) => ({ ok: true as const, registry }))
|
|
221
|
+
.catch((error: unknown) => ({
|
|
222
|
+
error,
|
|
223
|
+
ok: false as const,
|
|
224
|
+
})),
|
|
225
|
+
loadPrebuiltTocIndex(contentSource),
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
if (contentIndex.errors.length) {
|
|
229
|
+
return {
|
|
230
|
+
configErrors: contentIndex.errors,
|
|
231
|
+
configWarnings: configResult.warnings,
|
|
232
|
+
tenant,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!registryResult.ok) {
|
|
237
|
+
return {
|
|
238
|
+
configErrors: [
|
|
239
|
+
registryResult.error instanceof Error
|
|
240
|
+
? registryResult.error.message
|
|
241
|
+
: "OpenAPI parsing failed",
|
|
242
|
+
],
|
|
243
|
+
configWarnings: configResult.warnings,
|
|
244
|
+
tenant,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const pageMetadataMap = buildPageMetadataMap(contentIndex);
|
|
249
|
+
const slugPrefix = docsCollection?.slugPrefix ?? "";
|
|
250
|
+
const rawNav = docsNavigation
|
|
251
|
+
? buildNavigation(docsNavigation, registryResult.registry, slugPrefix)
|
|
252
|
+
: [];
|
|
253
|
+
const nav = enrichNavWithMetadata(rawNav, pageMetadataMap);
|
|
254
|
+
const visibleNav = getVisibleNavigation(nav);
|
|
255
|
+
const flatNav = flattenNav(nav);
|
|
256
|
+
const visibleFlatNav = flattenNav(visibleNav);
|
|
257
|
+
const rawTabs = buildTabbedNavigation(
|
|
258
|
+
docsNavigation,
|
|
259
|
+
registryResult.registry,
|
|
260
|
+
slugPrefix
|
|
261
|
+
);
|
|
262
|
+
const tabs =
|
|
263
|
+
rawTabs?.map((tab) => ({
|
|
264
|
+
...tab,
|
|
265
|
+
entries: enrichNavWithMetadata(tab.entries, pageMetadataMap),
|
|
266
|
+
})) ?? null;
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
anchors: docsNavigation?.global?.anchors ?? [],
|
|
270
|
+
config,
|
|
271
|
+
contentIndex,
|
|
272
|
+
contentSource,
|
|
273
|
+
flatNav,
|
|
274
|
+
pageMetadataMap,
|
|
275
|
+
registry: registryResult.registry,
|
|
276
|
+
tabs,
|
|
277
|
+
tenant,
|
|
278
|
+
tocBySlug: prebuiltTocIndex ?? new Map(),
|
|
279
|
+
visibleFlatNav,
|
|
280
|
+
visibleNav,
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export const getTenantSearchItems = cache(async (tenantSlug: string) => {
|
|
286
|
+
const artifacts = await getTenantArtifacts(tenantSlug);
|
|
287
|
+
if (
|
|
288
|
+
!artifacts ||
|
|
289
|
+
isConfigErrorResult(artifacts) ||
|
|
290
|
+
isUnpublishedTenantResult(artifacts)
|
|
291
|
+
) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const cacheKey = `${getTenantArtifactsCacheKey(artifacts.tenant)}:search`;
|
|
296
|
+
|
|
297
|
+
return await searchItemsCache.getOrCreate(cacheKey, async () => {
|
|
298
|
+
const prebuiltSearchIndex = await loadPrebuiltSearchIndex(
|
|
299
|
+
artifacts.contentSource
|
|
300
|
+
);
|
|
301
|
+
const utilityIndex = prebuiltSearchIndex
|
|
302
|
+
? null
|
|
303
|
+
: ((await loadPrebuiltUtilityIndex(artifacts.contentSource)) ??
|
|
304
|
+
(await buildUtilityIndex(
|
|
305
|
+
artifacts.contentIndex,
|
|
306
|
+
artifacts.contentSource,
|
|
307
|
+
artifacts.config
|
|
308
|
+
)));
|
|
309
|
+
|
|
310
|
+
return mergeSearchItems({
|
|
311
|
+
baseItems:
|
|
312
|
+
prebuiltSearchIndex ??
|
|
313
|
+
buildSearchItems({
|
|
314
|
+
config: artifacts.config,
|
|
315
|
+
contentIndex: artifacts.contentIndex,
|
|
316
|
+
utilityIndex,
|
|
317
|
+
}),
|
|
318
|
+
config: artifacts.config,
|
|
319
|
+
flatNav: artifacts.flatNav,
|
|
320
|
+
visibleFlatNav: artifacts.visibleFlatNav,
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const getRenderedPageData = async ({
|
|
326
|
+
artifacts,
|
|
327
|
+
currentPath,
|
|
328
|
+
relativePath,
|
|
329
|
+
rawContent: preloadedRawContent,
|
|
330
|
+
toc: preloadedToc,
|
|
331
|
+
useToc,
|
|
332
|
+
}: {
|
|
333
|
+
artifacts: TenantArtifacts;
|
|
334
|
+
currentPath: string;
|
|
335
|
+
relativePath: string;
|
|
336
|
+
rawContent?: string;
|
|
337
|
+
toc?: TocItem[];
|
|
338
|
+
useToc: boolean;
|
|
339
|
+
}) => {
|
|
340
|
+
const cacheKey = `${getTenantArtifactsCacheKey(artifacts.tenant)}:${currentPath}`;
|
|
341
|
+
|
|
342
|
+
return await renderedPageCache.getOrCreate(cacheKey, async () => {
|
|
343
|
+
const compiled =
|
|
344
|
+
await artifacts.contentSource.readCompiledMdx?.(relativePath);
|
|
345
|
+
const rawContent =
|
|
346
|
+
preloadedRawContent ??
|
|
347
|
+
(compiled
|
|
348
|
+
? undefined
|
|
349
|
+
: await loadContentSource(artifacts.contentSource, relativePath));
|
|
350
|
+
const rendered = compiled
|
|
351
|
+
? await renderFromCompiled(compiled.compiledSource)
|
|
352
|
+
: await renderMdx(rawContent ?? "");
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
content: rendered.content,
|
|
356
|
+
frontmatter: rendered.frontmatter,
|
|
357
|
+
rawContent,
|
|
358
|
+
toc: preloadedToc ?? (useToc && rawContent ? extractToc(rawContent) : []),
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
export const clearDocsRuntimeCaches = () => {
|
|
364
|
+
artifactsCache.clear();
|
|
365
|
+
searchItemsCache.clear();
|
|
366
|
+
renderedPageCache.clear();
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
export const clearDocsRuntimeCachesForTenant = (tenantId: string) => {
|
|
370
|
+
artifactsCache.deleteByPrefix(tenantId);
|
|
371
|
+
searchItemsCache.deleteByPrefix(tenantId);
|
|
372
|
+
renderedPageCache.deleteByPrefix(tenantId);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const computePrevNext = (
|
|
376
|
+
flatNav: NavPage[],
|
|
377
|
+
currentPath: string
|
|
378
|
+
): {
|
|
379
|
+
nextPage: { title: string; path: string } | undefined;
|
|
380
|
+
prevPage: { title: string; path: string } | undefined;
|
|
381
|
+
} => {
|
|
382
|
+
const currentIndex = flatNav.findIndex((p) => p.path === currentPath);
|
|
383
|
+
return {
|
|
384
|
+
nextPage:
|
|
385
|
+
currentIndex !== -1 && currentIndex < flatNav.length - 1
|
|
386
|
+
? flatNav[currentIndex + 1]
|
|
387
|
+
: undefined,
|
|
388
|
+
prevPage: currentIndex > 0 ? flatNav[currentIndex - 1] : undefined,
|
|
389
|
+
};
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Returns shell data (config, nav, title, breadcrumbs) from cached artifacts
|
|
394
|
+
* without compiling MDX. Used by the page component to render the shell
|
|
395
|
+
* immediately while streaming content.
|
|
396
|
+
*/
|
|
397
|
+
export const getDocShellData = cache(
|
|
398
|
+
// oxlint-disable-next-line eslint/complexity
|
|
399
|
+
async (tenantSlug: string, slugKey: string) => {
|
|
400
|
+
const artifacts = await getTenantArtifacts(tenantSlug);
|
|
401
|
+
if (
|
|
402
|
+
!artifacts ||
|
|
403
|
+
isConfigErrorResult(artifacts) ||
|
|
404
|
+
isUnpublishedTenantResult(artifacts)
|
|
405
|
+
) {
|
|
406
|
+
return artifacts;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const currentPath = normalizePath(slugKey) || "index";
|
|
410
|
+
const activeTabIndex = artifacts.tabs
|
|
411
|
+
? findActiveTabIndex(artifacts.tabs, currentPath)
|
|
412
|
+
: 0;
|
|
413
|
+
const activeTabNav = artifacts.tabs
|
|
414
|
+
? getVisibleNavigation(artifacts.tabs[activeTabIndex]?.entries ?? [])
|
|
415
|
+
: null;
|
|
416
|
+
const activeTabFlatNav = activeTabNav ? flattenNav(activeTabNav) : null;
|
|
417
|
+
const effectiveFlatNav = activeTabFlatNav ?? artifacts.visibleFlatNav;
|
|
418
|
+
|
|
419
|
+
const openApiEntry = artifacts.registry.bySlug.get(currentPath);
|
|
420
|
+
if (openApiEntry) {
|
|
421
|
+
const isHidden = artifacts.flatNav.some(
|
|
422
|
+
(page) => page.path === currentPath && page.hidden
|
|
423
|
+
);
|
|
424
|
+
return {
|
|
425
|
+
activeTabIndex,
|
|
426
|
+
anchors: artifacts.anchors,
|
|
427
|
+
breadcrumbs: findBreadcrumbs(
|
|
428
|
+
activeTabNav ?? artifacts.visibleNav,
|
|
429
|
+
currentPath
|
|
430
|
+
),
|
|
431
|
+
config: artifacts.config,
|
|
432
|
+
currentPath,
|
|
433
|
+
deprecated: false,
|
|
434
|
+
hidden: isHidden,
|
|
435
|
+
hideFooterPagination: false,
|
|
436
|
+
kind: "openapi" as const,
|
|
437
|
+
mode: undefined as PageMode | undefined,
|
|
438
|
+
nav: activeTabNav ?? artifacts.visibleNav,
|
|
439
|
+
nextPage: computePrevNext(effectiveFlatNav, currentPath).nextPage,
|
|
440
|
+
noindex: false,
|
|
441
|
+
openApiEntry,
|
|
442
|
+
openapiProxyEnabled: artifacts.config.openapiProxy?.enabled ?? false,
|
|
443
|
+
pageDescription: openApiEntry.operation.description,
|
|
444
|
+
pageTitle: openApiEntry.operation.summary ?? openApiEntry.identifier,
|
|
445
|
+
prevPage: computePrevNext(effectiveFlatNav, currentPath).prevPage,
|
|
446
|
+
tabs: artifacts.tabs,
|
|
447
|
+
tenant: artifacts.tenant,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const entry = artifacts.contentIndex.bySlug.get(currentPath) ?? null;
|
|
452
|
+
if (!entry) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (entry.kind === "index") {
|
|
457
|
+
const collectionEntries =
|
|
458
|
+
artifacts.contentIndex.byCollection
|
|
459
|
+
.get(entry.collectionId)
|
|
460
|
+
?.filter(
|
|
461
|
+
(
|
|
462
|
+
collectionEntry
|
|
463
|
+
): collectionEntry is Extract<
|
|
464
|
+
(typeof artifacts.contentIndex.entries)[number],
|
|
465
|
+
{ kind: "entry" }
|
|
466
|
+
> =>
|
|
467
|
+
collectionEntry.kind === "entry" &&
|
|
468
|
+
collectionEntry.hidden !== true &&
|
|
469
|
+
!artifacts.pageMetadataMap.get(collectionEntry.slug)?.hidden &&
|
|
470
|
+
!artifacts.pageMetadataMap.get(collectionEntry.slug)?.noindex
|
|
471
|
+
) ?? [];
|
|
472
|
+
const showDocsNav = entry.type === "docs";
|
|
473
|
+
return {
|
|
474
|
+
activeTabIndex,
|
|
475
|
+
anchors: showDocsNav ? artifacts.anchors : [],
|
|
476
|
+
breadcrumbs: [] as { label: string; path: string }[],
|
|
477
|
+
collectionIndex: { entries: collectionEntries },
|
|
478
|
+
config: artifacts.config,
|
|
479
|
+
currentPath,
|
|
480
|
+
deprecated: false,
|
|
481
|
+
hidden: false,
|
|
482
|
+
hideFooterPagination: false,
|
|
483
|
+
kind: "index" as const,
|
|
484
|
+
mode: undefined as PageMode | undefined,
|
|
485
|
+
nav: showDocsNav ? (activeTabNav ?? artifacts.visibleNav) : [],
|
|
486
|
+
nextPage: computePrevNext(effectiveFlatNav, currentPath).nextPage,
|
|
487
|
+
noindex: false,
|
|
488
|
+
pageDescription: entry.description,
|
|
489
|
+
pageTitle: entry.title,
|
|
490
|
+
prevPage: computePrevNext(effectiveFlatNav, currentPath).prevPage,
|
|
491
|
+
tabs: artifacts.tabs,
|
|
492
|
+
tenant: artifacts.tenant,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const showDocsNav = entry.type === "docs";
|
|
497
|
+
const breadcrumbs = showDocsNav
|
|
498
|
+
? findBreadcrumbs(activeTabNav ?? artifacts.visibleNav, currentPath)
|
|
499
|
+
: [];
|
|
500
|
+
const pageMeta = artifacts.pageMetadataMap.get(currentPath);
|
|
501
|
+
const isHiddenByNav = artifacts.flatNav.some(
|
|
502
|
+
(page) => page.path === currentPath && page.hidden
|
|
503
|
+
);
|
|
504
|
+
const { nextPage, prevPage } = computePrevNext(
|
|
505
|
+
effectiveFlatNav,
|
|
506
|
+
currentPath
|
|
507
|
+
);
|
|
508
|
+
const useToc =
|
|
509
|
+
entry.type === "docs" && artifacts.config.features?.toc !== false;
|
|
510
|
+
|
|
511
|
+
const prebuiltToc = artifacts.tocBySlug.get(currentPath) ?? null;
|
|
512
|
+
const needsRawContent = artifacts.config.contextual
|
|
513
|
+
? contextualOptionsRequirePageContent(artifacts.config.contextual.options)
|
|
514
|
+
: false;
|
|
515
|
+
|
|
516
|
+
let rawContent: string | undefined;
|
|
517
|
+
let toc: ReturnType<typeof extractToc> = prebuiltToc ?? [];
|
|
518
|
+
if ((!prebuiltToc && useToc) || needsRawContent) {
|
|
519
|
+
try {
|
|
520
|
+
rawContent = await loadContentSource(
|
|
521
|
+
artifacts.contentSource,
|
|
522
|
+
entry.relativePath
|
|
523
|
+
);
|
|
524
|
+
if (!prebuiltToc && useToc) {
|
|
525
|
+
toc = extractToc(rawContent);
|
|
526
|
+
}
|
|
527
|
+
} catch {
|
|
528
|
+
// Content load failure is handled by the content rendering path
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
activeTabIndex,
|
|
534
|
+
anchors: showDocsNav ? artifacts.anchors : [],
|
|
535
|
+
breadcrumbs,
|
|
536
|
+
config: artifacts.config,
|
|
537
|
+
currentPath,
|
|
538
|
+
deprecated: pageMeta?.deprecated ?? false,
|
|
539
|
+
hidden: isHiddenByNav || entry.hidden === true,
|
|
540
|
+
hideFooterPagination: pageMeta?.hideFooterPagination ?? false,
|
|
541
|
+
kind: "page" as const,
|
|
542
|
+
mode: pageMeta?.mode,
|
|
543
|
+
nav: showDocsNav ? (activeTabNav ?? artifacts.visibleNav) : [],
|
|
544
|
+
nextPage,
|
|
545
|
+
noindex: pageMeta?.noindex ?? false,
|
|
546
|
+
pageDescription: entry.description,
|
|
547
|
+
pageTitle: entry.title,
|
|
548
|
+
prevPage,
|
|
549
|
+
rawContent,
|
|
550
|
+
tabs: artifacts.tabs,
|
|
551
|
+
tenant: artifacts.tenant,
|
|
552
|
+
toc,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Compiles MDX content for a specific page. Uses pre-compiled content
|
|
559
|
+
* from deploy when available, otherwise falls back to runtime compilation.
|
|
560
|
+
*/
|
|
561
|
+
export const getDocPageContent = cache(
|
|
562
|
+
async (
|
|
563
|
+
tenantSlug: string,
|
|
564
|
+
slugKey: string,
|
|
565
|
+
rawContent?: string,
|
|
566
|
+
toc?: TocItem[]
|
|
567
|
+
) => {
|
|
568
|
+
const artifacts = await getTenantArtifacts(tenantSlug);
|
|
569
|
+
if (
|
|
570
|
+
!artifacts ||
|
|
571
|
+
isConfigErrorResult(artifacts) ||
|
|
572
|
+
isUnpublishedTenantResult(artifacts)
|
|
573
|
+
) {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const currentPath = normalizePath(slugKey) || "index";
|
|
578
|
+
const entry = artifacts.contentIndex.bySlug.get(currentPath) ?? null;
|
|
579
|
+
if (!entry || entry.kind !== "entry") {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const useToc =
|
|
584
|
+
entry.type === "docs" && artifacts.config.features?.toc !== false;
|
|
585
|
+
|
|
586
|
+
return await getRenderedPageData({
|
|
587
|
+
artifacts,
|
|
588
|
+
currentPath,
|
|
589
|
+
rawContent,
|
|
590
|
+
relativePath: entry.relativePath,
|
|
591
|
+
toc,
|
|
592
|
+
useToc,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TenantEdgeHostRecord,
|
|
3
|
+
TenantEdgeSlugRecord,
|
|
4
|
+
} from "@repo/contracts";
|
|
5
|
+
import {
|
|
6
|
+
TenantEdgeHostRecordSchema,
|
|
7
|
+
TenantEdgeSlugRecordSchema,
|
|
8
|
+
getTenantEdgeHostKeys,
|
|
9
|
+
getTenantEdgeHostKey,
|
|
10
|
+
getTenantEdgeSlugKeys,
|
|
11
|
+
getTenantEdgeSlugKey,
|
|
12
|
+
} from "@repo/contracts";
|
|
13
|
+
import { createClient } from "@vercel/edge-config";
|
|
14
|
+
|
|
15
|
+
import { createTimedPromiseCache } from "./server-cache";
|
|
16
|
+
|
|
17
|
+
const EDGE_CONFIG_CACHE_TTL_MS = 30 * 1000;
|
|
18
|
+
|
|
19
|
+
const readTrimmedEnv = (name: string) => {
|
|
20
|
+
const value = process.env[name];
|
|
21
|
+
if (typeof value !== "string") {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
if (!trimmed) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
return trimmed;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const edgeConfigConnectionString = readTrimmedEnv("EDGE_CONFIG");
|
|
32
|
+
const edgeConfigClient = edgeConfigConnectionString
|
|
33
|
+
? createClient(edgeConfigConnectionString)
|
|
34
|
+
: null;
|
|
35
|
+
|
|
36
|
+
export { getTenantEdgeHostKey, getTenantEdgeSlugKey };
|
|
37
|
+
|
|
38
|
+
const hostRecordCache = createTimedPromiseCache<
|
|
39
|
+
string,
|
|
40
|
+
TenantEdgeHostRecord | null
|
|
41
|
+
>({
|
|
42
|
+
maxEntries: 512,
|
|
43
|
+
ttlMs: EDGE_CONFIG_CACHE_TTL_MS,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const slugRecordCache = createTimedPromiseCache<
|
|
47
|
+
string,
|
|
48
|
+
TenantEdgeSlugRecord | null
|
|
49
|
+
>({
|
|
50
|
+
maxEntries: 512,
|
|
51
|
+
ttlMs: EDGE_CONFIG_CACHE_TTL_MS,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const getEdgeConfigValue = async (key: string) => {
|
|
55
|
+
if (!edgeConfigClient) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
return (await edgeConfigClient.get(key)) as unknown;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const isEdgeConfigEnabled = () => Boolean(edgeConfigClient);
|
|
67
|
+
|
|
68
|
+
export const clearTenantEdgeConfigCaches = () => {
|
|
69
|
+
hostRecordCache.clear();
|
|
70
|
+
slugRecordCache.clear();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const getTenantEdgeHostRecord = async (host: string) =>
|
|
74
|
+
await hostRecordCache.getOrCreate(getTenantEdgeHostKey(host), async () => {
|
|
75
|
+
for (const key of getTenantEdgeHostKeys(host)) {
|
|
76
|
+
const value = await getEdgeConfigValue(key);
|
|
77
|
+
const parsed = TenantEdgeHostRecordSchema.safeParse(value);
|
|
78
|
+
if (parsed.success) {
|
|
79
|
+
return parsed.data;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const getTenantEdgeSlugRecord = async (slug: string) =>
|
|
86
|
+
await slugRecordCache.getOrCreate(getTenantEdgeSlugKey(slug), async () => {
|
|
87
|
+
for (const key of getTenantEdgeSlugKeys(slug)) {
|
|
88
|
+
const value = await getEdgeConfigValue(key);
|
|
89
|
+
const parsed = TenantEdgeSlugRecordSchema.safeParse(value);
|
|
90
|
+
if (parsed.success) {
|
|
91
|
+
return parsed.data;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
});
|
package/docs/lib/env.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const readTrimmedEnv = (name: string) => {
|
|
2
|
+
const value = process.env[name];
|
|
3
|
+
if (typeof value !== "string") {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
if (!trimmed) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
return trimmed;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const docsApiBase =
|
|
14
|
+
readTrimmedEnv("DOCS_API_URL") ??
|
|
15
|
+
readTrimmedEnv("NEXT_PUBLIC_API_URL") ??
|
|
16
|
+
"http://localhost:4000";
|
|
17
|
+
|
|
18
|
+
export const platformAssetPrefix =
|
|
19
|
+
readTrimmedEnv("PLATFORM_ASSET_PREFIX") ?? "";
|
|
20
|
+
|
|
21
|
+
export const platformRootDomain =
|
|
22
|
+
readTrimmedEnv("PLATFORM_ROOT_DOMAIN") ?? "blode.md";
|