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.
Files changed (61) hide show
  1. package/README.md +25 -9
  2. package/dev-server/app/[[...slug]]/page.tsx +1 -0
  3. package/dev-server/next.config.js +11 -13
  4. package/dev-server/package.json +1 -1
  5. package/dev-server/tsconfig.json +3 -0
  6. package/dist/cli.mjs +869 -184
  7. package/dist/cli.mjs.map +1 -1
  8. package/docs/components/api/api-playground.tsx +255 -80
  9. package/docs/components/api/api-reference.tsx +11 -1
  10. package/docs/components/docs/contextual-menu.tsx +227 -142
  11. package/docs/components/docs/copy-page-menu.tsx +132 -85
  12. package/docs/components/docs/doc-header.tsx +13 -3
  13. package/docs/components/docs/doc-shell.tsx +22 -11
  14. package/docs/components/docs/mobile-nav.tsx +0 -6
  15. package/docs/components/mdx/code-group.tsx +171 -62
  16. package/docs/components/mdx/tabs.tsx +131 -26
  17. package/docs/components/ui/input.tsx +0 -1
  18. package/docs/components/ui/search.tsx +241 -132
  19. package/docs/lib/content-root.ts +33 -0
  20. package/docs/lib/content-source.ts +70 -0
  21. package/docs/lib/contextual-options.ts +20 -0
  22. package/docs/lib/docs-runtime.tsx +595 -0
  23. package/docs/lib/edge-config.ts +95 -0
  24. package/docs/lib/env.ts +22 -0
  25. package/docs/lib/openapi-proxy.ts +88 -0
  26. package/docs/lib/platform-config.ts +6 -0
  27. package/docs/lib/routes.ts +39 -0
  28. package/docs/lib/supabase.ts +13 -0
  29. package/docs/lib/tenancy.ts +322 -0
  30. package/docs/lib/tenant-headers.ts +14 -0
  31. package/docs/lib/tenant-static.ts +529 -0
  32. package/docs/lib/tenant-utility-context.ts +62 -0
  33. package/docs/lib/tenants.ts +68 -0
  34. package/docs/lib/use-mobile.ts +19 -0
  35. package/package.json +3 -2
  36. package/packages/@repo/common/dist/index.d.ts +7 -0
  37. package/packages/@repo/common/dist/index.d.ts.map +1 -1
  38. package/packages/@repo/common/dist/index.js +42 -0
  39. package/packages/@repo/common/src/index.ts +50 -0
  40. package/packages/@repo/contracts/dist/project.d.ts +1 -1
  41. package/packages/@repo/contracts/dist/project.js +1 -1
  42. package/packages/@repo/contracts/src/project.ts +1 -1
  43. package/packages/@repo/models/dist/docs-config.d.ts +194 -29
  44. package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
  45. package/packages/@repo/models/dist/docs-config.js +3 -28
  46. package/packages/@repo/models/src/docs-config.ts +5 -31
  47. package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
  48. package/packages/@repo/previewing/dist/blob-source.js +7 -2
  49. package/packages/@repo/previewing/dist/fs-source.d.ts.map +1 -1
  50. package/packages/@repo/previewing/dist/fs-source.js +2 -3
  51. package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
  52. package/packages/@repo/previewing/dist/index.js +1 -41
  53. package/packages/@repo/previewing/src/blob-source.ts +7 -4
  54. package/packages/@repo/previewing/src/fs-source.ts +2 -3
  55. package/packages/@repo/previewing/src/index.ts +3 -55
  56. package/packages/@repo/validation/dist/index.d.ts +2 -2
  57. package/packages/@repo/validation/dist/index.d.ts.map +1 -1
  58. package/packages/@repo/validation/dist/index.js +2 -2
  59. package/packages/@repo/validation/package.json +1 -0
  60. package/packages/@repo/validation/src/{mintlify-docs-schema.json → blodemd-docs-schema.json} +346 -1794
  61. 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
+ });
@@ -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";