blodemd 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +1 -1
  2. package/dev-server/lib/local-runtime.tsx +12 -15
  3. package/dev-server/package.json +5 -1
  4. package/dist/cli.mjs +1193 -978
  5. package/dist/cli.mjs.map +1 -1
  6. package/docs/app/globals.css +1 -1
  7. package/docs/components/api/api-playground.tsx +2 -5
  8. package/docs/components/api/api-reference.tsx +29 -3
  9. package/docs/components/docs/contextual-menu.tsx +37 -33
  10. package/docs/components/docs/copy-page-menu.tsx +18 -0
  11. package/docs/components/docs/doc-header.tsx +4 -1
  12. package/docs/components/docs/doc-shell.tsx +48 -18
  13. package/docs/components/docs/doc-sidebar.tsx +131 -89
  14. package/docs/components/docs/doc-toc.tsx +1 -0
  15. package/docs/components/docs/mobile-nav.tsx +72 -68
  16. package/docs/components/docs/sidebar-active-highlight.tsx +34 -0
  17. package/docs/components/docs/sidebar-scroll-area.tsx +9 -4
  18. package/docs/components/git/repo-picker.tsx +5 -2
  19. package/docs/components/mdx/index.tsx +31 -21
  20. package/docs/components/posthog-provider.tsx +42 -0
  21. package/docs/components/tenant-analytics.tsx +49 -0
  22. package/docs/components/third-parties.tsx +4 -1
  23. package/docs/components/ui/copy-button.tsx +16 -2
  24. package/docs/components/ui/search-dialog.tsx +431 -0
  25. package/docs/components/ui/search.tsx +44 -427
  26. package/docs/components/web-mcp.tsx +115 -0
  27. package/docs/lib/agent-skills.ts +48 -0
  28. package/docs/lib/config.ts +4 -5
  29. package/docs/lib/docs-collection.ts +7 -22
  30. package/docs/lib/docs-runtime.tsx +160 -10
  31. package/docs/lib/llms-full.ts +82 -0
  32. package/docs/lib/marketing-markdown.ts +79 -0
  33. package/docs/lib/marketing-site.ts +31 -0
  34. package/docs/lib/mdx.ts +17 -6
  35. package/docs/lib/routes.ts +19 -3
  36. package/docs/lib/shiki.ts +4 -0
  37. package/docs/lib/tenancy.ts +15 -2
  38. package/docs/lib/tenant-headers.ts +29 -0
  39. package/docs/lib/tenant-static.ts +73 -99
  40. package/package.json +8 -5
  41. package/packages/@repo/common/package.json +1 -1
  42. package/packages/@repo/contracts/dist/analytics.d.ts +21 -0
  43. package/packages/@repo/contracts/dist/analytics.d.ts.map +1 -0
  44. package/packages/@repo/contracts/dist/analytics.js +18 -0
  45. package/packages/@repo/contracts/dist/deployment.js +1 -1
  46. package/packages/@repo/contracts/dist/git.js +2 -2
  47. package/packages/@repo/contracts/dist/ids.d.ts.map +1 -1
  48. package/packages/@repo/contracts/dist/ids.js +2 -1
  49. package/packages/@repo/contracts/dist/index.d.ts +1 -0
  50. package/packages/@repo/contracts/dist/index.d.ts.map +1 -1
  51. package/packages/@repo/contracts/dist/index.js +1 -0
  52. package/packages/@repo/contracts/dist/project.d.ts +18 -1
  53. package/packages/@repo/contracts/dist/project.d.ts.map +1 -1
  54. package/packages/@repo/contracts/dist/project.js +3 -1
  55. package/packages/@repo/contracts/dist/tenant.d.ts +36 -0
  56. package/packages/@repo/contracts/dist/tenant.d.ts.map +1 -1
  57. package/packages/@repo/contracts/dist/tenant.js +2 -0
  58. package/packages/@repo/contracts/package.json +1 -1
  59. package/packages/@repo/contracts/src/analytics.ts +33 -0
  60. package/packages/@repo/contracts/src/deployment.ts +1 -1
  61. package/packages/@repo/contracts/src/git.ts +2 -2
  62. package/packages/@repo/contracts/src/ids.ts +2 -1
  63. package/packages/@repo/contracts/src/index.ts +1 -0
  64. package/packages/@repo/contracts/src/project.ts +3 -1
  65. package/packages/@repo/contracts/src/tenant.ts +2 -0
  66. package/packages/@repo/models/dist/docs-config.d.ts +0 -198
  67. package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
  68. package/packages/@repo/models/dist/docs-config.js +3 -2
  69. package/packages/@repo/models/dist/tenant.d.ts +12 -0
  70. package/packages/@repo/models/dist/tenant.d.ts.map +1 -1
  71. package/packages/@repo/models/package.json +1 -1
  72. package/packages/@repo/models/src/docs-config.ts +3 -3
  73. package/packages/@repo/models/src/tenant.ts +15 -0
  74. package/packages/@repo/prebuild/dist/openapi.d.ts.map +1 -1
  75. package/packages/@repo/prebuild/dist/openapi.js +32 -4
  76. package/packages/@repo/prebuild/package.json +1 -1
  77. package/packages/@repo/prebuild/src/openapi.ts +42 -4
  78. package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
  79. package/packages/@repo/previewing/dist/blob-source.js +18 -5
  80. package/packages/@repo/previewing/dist/config-loader.d.ts +134 -0
  81. package/packages/@repo/previewing/dist/config-loader.d.ts.map +1 -0
  82. package/packages/@repo/previewing/dist/config-loader.js +231 -0
  83. package/packages/@repo/previewing/dist/constants.d.ts +15 -0
  84. package/packages/@repo/previewing/dist/constants.d.ts.map +1 -0
  85. package/packages/@repo/previewing/dist/constants.js +14 -0
  86. package/packages/@repo/previewing/dist/content-index.d.ts +7 -0
  87. package/packages/@repo/previewing/dist/content-index.d.ts.map +1 -0
  88. package/packages/@repo/previewing/dist/content-index.js +291 -0
  89. package/packages/@repo/previewing/dist/index.d.ts +13 -121
  90. package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
  91. package/packages/@repo/previewing/dist/index.js +11 -1003
  92. package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts +3 -0
  93. package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts.map +1 -0
  94. package/packages/@repo/previewing/dist/markdown/agent-markdown.js +176 -0
  95. package/packages/@repo/previewing/dist/markdown/format.d.ts +8 -0
  96. package/packages/@repo/previewing/dist/markdown/format.d.ts.map +1 -0
  97. package/packages/@repo/previewing/dist/markdown/format.js +43 -0
  98. package/packages/@repo/previewing/dist/markdown/links.d.ts +3 -0
  99. package/packages/@repo/previewing/dist/markdown/links.d.ts.map +1 -0
  100. package/packages/@repo/previewing/dist/markdown/links.js +31 -0
  101. package/packages/@repo/previewing/dist/openapi-pages.d.ts +7 -0
  102. package/packages/@repo/previewing/dist/openapi-pages.d.ts.map +1 -0
  103. package/packages/@repo/previewing/dist/openapi-pages.js +200 -0
  104. package/packages/@repo/previewing/dist/search-index.d.ts +4 -0
  105. package/packages/@repo/previewing/dist/search-index.d.ts.map +1 -0
  106. package/packages/@repo/previewing/dist/search-index.js +23 -0
  107. package/packages/@repo/previewing/dist/serialization.d.ts +13 -0
  108. package/packages/@repo/previewing/dist/serialization.d.ts.map +1 -0
  109. package/packages/@repo/previewing/dist/serialization.js +108 -0
  110. package/packages/@repo/previewing/dist/toc-index.d.ts +5 -0
  111. package/packages/@repo/previewing/dist/toc-index.d.ts.map +1 -0
  112. package/packages/@repo/previewing/dist/toc-index.js +53 -0
  113. package/packages/@repo/previewing/dist/types.d.ts +92 -0
  114. package/packages/@repo/previewing/dist/types.d.ts.map +1 -0
  115. package/packages/@repo/previewing/dist/types.js +1 -0
  116. package/packages/@repo/previewing/dist/utility-index.d.ts +7 -0
  117. package/packages/@repo/previewing/dist/utility-index.d.ts.map +1 -0
  118. package/packages/@repo/previewing/dist/utility-index.js +201 -0
  119. package/packages/@repo/previewing/package.json +1 -1
  120. package/packages/@repo/previewing/src/blob-source.ts +20 -5
  121. package/packages/@repo/previewing/src/config-loader.ts +316 -0
  122. package/packages/@repo/previewing/src/constants.ts +16 -0
  123. package/packages/@repo/previewing/src/content-index.ts +384 -0
  124. package/packages/@repo/previewing/src/index.ts +69 -1515
  125. package/packages/@repo/previewing/src/markdown/agent-markdown.ts +254 -0
  126. package/packages/@repo/previewing/src/markdown/format.ts +63 -0
  127. package/packages/@repo/previewing/src/markdown/links.ts +46 -0
  128. package/packages/@repo/previewing/src/openapi-pages.ts +319 -0
  129. package/packages/@repo/previewing/src/search-index.ts +38 -0
  130. package/packages/@repo/previewing/src/serialization.ts +191 -0
  131. package/packages/@repo/previewing/src/toc-index.ts +72 -0
  132. package/packages/@repo/previewing/src/types.ts +105 -0
  133. package/packages/@repo/previewing/src/utility-index.ts +304 -0
  134. package/packages/@repo/validation/package.json +1 -1
  135. package/scripts/repo-packages.mjs +8 -0
  136. package/docs/components/ui/site-footer.tsx +0 -41
@@ -1,22 +1,7 @@
1
- import type { SiteConfig } from "@repo/models";
2
-
3
- export const getDocsCollection = (
4
- config: SiteConfig
5
- ): SiteConfig["collections"][number] | undefined =>
6
- config.collections.find((collection) => collection.type === "docs");
7
-
8
- export const getDocsNavigation = (config: SiteConfig) =>
9
- getDocsCollection(config)?.navigation ?? config.navigation;
10
-
11
- export const getDocsCollectionWithNavigation = (
12
- config: SiteConfig
13
- ): SiteConfig["collections"][number] | undefined => {
14
- const docsCollection = getDocsCollection(config);
15
- const docsNavigation = getDocsNavigation(config);
16
-
17
- return docsCollection &&
18
- docsNavigation &&
19
- docsCollection.navigation !== docsNavigation
20
- ? { ...docsCollection, navigation: docsNavigation }
21
- : docsCollection;
22
- };
1
+ // These helpers live in @repo/previewing; re-exported here so existing docs
2
+ // imports keep working against the single shared implementation.
3
+ export {
4
+ getDocsCollection,
5
+ getDocsCollectionWithNavigation,
6
+ getDocsNavigation,
7
+ } from "@repo/previewing";
@@ -5,7 +5,6 @@ import {
5
5
  buildPageMetadataMap,
6
6
  buildSearchIndex,
7
7
  buildUtilityIndex,
8
- loadContentSource,
9
8
  loadPrebuiltContentIndex,
10
9
  loadPrebuiltSearchIndex,
11
10
  loadPrebuiltTocIndex,
@@ -282,6 +281,36 @@ const getTenantArtifacts = async (tenantSlug: string) => {
282
281
  });
283
282
  };
284
283
 
284
+ export interface TenantSidebarData {
285
+ anchors: { label: string; href: string }[];
286
+ entries: NavEntry[];
287
+ }
288
+
289
+ export const getTenantSidebarData = cache(
290
+ async (
291
+ tenantSlug: string,
292
+ activeTabIndex: number
293
+ ): Promise<TenantSidebarData | null> => {
294
+ const artifacts = await getTenantArtifacts(tenantSlug);
295
+ if (
296
+ !artifacts ||
297
+ isConfigErrorResult(artifacts) ||
298
+ isUnpublishedTenantResult(artifacts)
299
+ ) {
300
+ return null;
301
+ }
302
+
303
+ const tabEntries = artifacts.tabs
304
+ ? getVisibleNavigation(artifacts.tabs[activeTabIndex]?.entries ?? [])
305
+ : null;
306
+
307
+ return {
308
+ anchors: artifacts.anchors,
309
+ entries: tabEntries ?? artifacts.visibleNav,
310
+ };
311
+ }
312
+ );
313
+
285
314
  export const getTenantSearchItems = cache(async (tenantSlug: string) => {
286
315
  const artifacts = await getTenantArtifacts(tenantSlug);
287
316
  if (
@@ -324,6 +353,7 @@ export const getTenantSearchItems = cache(async (tenantSlug: string) => {
324
353
 
325
354
  const getRenderedPageData = async ({
326
355
  artifacts,
356
+ basePath,
327
357
  currentPath,
328
358
  relativePath,
329
359
  rawContent: preloadedRawContent,
@@ -331,13 +361,14 @@ const getRenderedPageData = async ({
331
361
  useToc,
332
362
  }: {
333
363
  artifacts: TenantArtifacts;
364
+ basePath?: string;
334
365
  currentPath: string;
335
366
  relativePath: string;
336
367
  rawContent?: string;
337
368
  toc?: TocItem[];
338
369
  useToc: boolean;
339
370
  }) => {
340
- const cacheKey = `${getTenantArtifactsCacheKey(artifacts.tenant)}:${currentPath}`;
371
+ const cacheKey = `${getTenantArtifactsCacheKey(artifacts.tenant)}:${basePath ?? ""}:${currentPath}`;
341
372
 
342
373
  return await renderedPageCache.getOrCreate(cacheKey, async () => {
343
374
  const compiled =
@@ -346,10 +377,14 @@ const getRenderedPageData = async ({
346
377
  preloadedRawContent ??
347
378
  (compiled
348
379
  ? undefined
349
- : await loadContentSource(artifacts.contentSource, relativePath));
380
+ : await artifacts.contentSource.readFile(relativePath));
350
381
  const rendered = compiled
351
- ? await renderFromCompiled(compiled.compiledSource)
352
- : await renderMdx(rawContent ?? "");
382
+ ? await renderFromCompiled(
383
+ compiled.compiledSource,
384
+ basePath,
385
+ relativePath
386
+ )
387
+ : await renderMdx(rawContent ?? "", basePath, relativePath);
353
388
 
354
389
  return {
355
390
  content: rendered.content,
@@ -389,6 +424,103 @@ const computePrevNext = (
389
424
  };
390
425
  };
391
426
 
427
+ const META_DESCRIPTION_MIN = 120;
428
+ const META_DESCRIPTION_MAX = 160;
429
+ const META_FRONTMATTER_RE = /^---\s*\n[\s\S]*?\n---\s*\n?/;
430
+
431
+ /**
432
+ * Reduce raw MDX/Markdown to a single line of readable prose so it can seed a
433
+ * meta description when a page has no (or a too-short) frontmatter description.
434
+ */
435
+ const markdownToPlainText = (markdown: string): string =>
436
+ markdown
437
+ .replace(META_FRONTMATTER_RE, "")
438
+ .replaceAll(/```[\s\S]*?```/g, " ")
439
+ .replaceAll(/~~~[\s\S]*?~~~/g, " ")
440
+ .replaceAll(/`[^`]*`/g, " ")
441
+ .replaceAll(/!\[[^\]]*\]\([^)]*\)/g, " ")
442
+ .replaceAll(/\[([^\]]+)\]\([^)]*\)/g, "$1")
443
+ .replaceAll(/<[^>]+>/g, " ")
444
+ .replaceAll(/^\s{0,3}#{1,6}\s+/gm, "")
445
+ .replaceAll(/^\s{0,3}>\s?/gm, "")
446
+ .replaceAll(/^\s{0,3}(?:[-*+]|\d+\.)\s+/gm, "")
447
+ .replaceAll(/[*_~]+/g, "")
448
+ .replaceAll("|", " ")
449
+ .replaceAll(/\s+/g, " ")
450
+ .trim();
451
+
452
+ const normalizeMetaText = (value?: string) =>
453
+ value ? value.replaceAll(/\s+/g, " ").trim() : "";
454
+
455
+ const truncateMetaDescription = (text: string, max: number): string => {
456
+ if (text.length <= max) {
457
+ return text;
458
+ }
459
+ const slice = text.slice(0, max - 1);
460
+ const lastSpace = slice.lastIndexOf(" ");
461
+ const trimmed = (lastSpace > max * 0.6 ? slice.slice(0, lastSpace) : slice)
462
+ .replace(/[\s.,;:–—-]+$/, "")
463
+ .trim();
464
+ return `${trimmed}…`;
465
+ };
466
+
467
+ /**
468
+ * Build a 120-160 character meta description from (in order of preference) the
469
+ * frontmatter description, the page body, and a site-level fallback. Keeps the
470
+ * visible page subtitle untouched while giving crawlers a complete description.
471
+ */
472
+ const deriveMetaDescription = ({
473
+ description,
474
+ rawText,
475
+ fallback,
476
+ }: {
477
+ description?: string;
478
+ rawText?: string;
479
+ fallback?: string;
480
+ }): string | undefined => {
481
+ const frontmatter = normalizeMetaText(description);
482
+ if (frontmatter.length >= META_DESCRIPTION_MIN) {
483
+ return truncateMetaDescription(frontmatter, META_DESCRIPTION_MAX);
484
+ }
485
+
486
+ let combined = frontmatter;
487
+ const excerpt = rawText ? markdownToPlainText(rawText) : "";
488
+ if (excerpt) {
489
+ if (
490
+ !frontmatter ||
491
+ excerpt.toLowerCase().startsWith(frontmatter.toLowerCase())
492
+ ) {
493
+ combined = excerpt;
494
+ } else if (combined.length < META_DESCRIPTION_MIN) {
495
+ combined = `${frontmatter} ${excerpt}`;
496
+ }
497
+ }
498
+
499
+ const fallbackText = normalizeMetaText(fallback);
500
+ if (
501
+ fallbackText &&
502
+ normalizeMetaText(combined).length < META_DESCRIPTION_MIN
503
+ ) {
504
+ combined = combined ? `${combined} ${fallbackText}` : fallbackText;
505
+ }
506
+
507
+ combined = normalizeMetaText(combined);
508
+ return combined
509
+ ? truncateMetaDescription(combined, META_DESCRIPTION_MAX)
510
+ : undefined;
511
+ };
512
+
513
+ const buildMetaDescriptionFallback = (
514
+ config: { name?: string; description?: string } | undefined,
515
+ title: string | undefined
516
+ ): string => {
517
+ const name = config?.name ?? "Docs";
518
+ const lead = title
519
+ ? `${title} — ${name} documentation.`
520
+ : `${name} documentation.`;
521
+ return config?.description ? `${lead} ${config.description}` : lead;
522
+ };
523
+
392
524
  /**
393
525
  * Returns shell data (config, nav, title, breadcrumbs) from cached artifacts
394
526
  * without compiling MDX. Used by the page component to render the shell
@@ -434,6 +566,13 @@ export const getDocShellData = cache(
434
566
  hidden: isHidden,
435
567
  hideFooterPagination: false,
436
568
  kind: "openapi" as const,
569
+ metaDescription: deriveMetaDescription({
570
+ description: openApiEntry.operation.description,
571
+ fallback: buildMetaDescriptionFallback(
572
+ artifacts.config,
573
+ openApiEntry.operation.summary ?? openApiEntry.identifier
574
+ ),
575
+ }),
437
576
  mode: undefined as PageMode | undefined,
438
577
  nav: activeTabNav ?? artifacts.visibleNav,
439
578
  nextPage: computePrevNext(effectiveFlatNav, currentPath).nextPage,
@@ -481,6 +620,10 @@ export const getDocShellData = cache(
481
620
  hidden: false,
482
621
  hideFooterPagination: false,
483
622
  kind: "index" as const,
623
+ metaDescription: deriveMetaDescription({
624
+ description: entry.description,
625
+ fallback: buildMetaDescriptionFallback(artifacts.config, entry.title),
626
+ }),
484
627
  mode: undefined as PageMode | undefined,
485
628
  nav: showDocsNav ? (activeTabNav ?? artifacts.visibleNav) : [],
486
629
  nextPage: computePrevNext(effectiveFlatNav, currentPath).nextPage,
@@ -513,14 +656,14 @@ export const getDocShellData = cache(
513
656
  ? contextualOptionsRequirePageContent(artifacts.config.contextual.options)
514
657
  : false;
515
658
 
659
+ // Load the body when we need a TOC, contextual content, or a body excerpt
660
+ // to flesh out a short/missing frontmatter description for SEO.
661
+ const needsExcerpt = (entry.description ?? "").trim().length < 120;
516
662
  let rawContent: string | undefined;
517
663
  let toc: ReturnType<typeof extractToc> = prebuiltToc ?? [];
518
- if ((!prebuiltToc && useToc) || needsRawContent) {
664
+ if ((!prebuiltToc && useToc) || needsRawContent || needsExcerpt) {
519
665
  try {
520
- rawContent = await loadContentSource(
521
- artifacts.contentSource,
522
- entry.relativePath
523
- );
666
+ rawContent = await artifacts.contentSource.readFile(entry.relativePath);
524
667
  if (!prebuiltToc && useToc) {
525
668
  toc = extractToc(rawContent);
526
669
  }
@@ -539,6 +682,11 @@ export const getDocShellData = cache(
539
682
  hidden: isHiddenByNav || entry.hidden === true,
540
683
  hideFooterPagination: pageMeta?.hideFooterPagination ?? false,
541
684
  kind: "page" as const,
685
+ metaDescription: deriveMetaDescription({
686
+ description: entry.description,
687
+ fallback: buildMetaDescriptionFallback(artifacts.config, entry.title),
688
+ rawText: rawContent,
689
+ }),
542
690
  mode: pageMeta?.mode,
543
691
  nav: showDocsNav ? (activeTabNav ?? artifacts.visibleNav) : [],
544
692
  nextPage,
@@ -562,6 +710,7 @@ export const getDocPageContent = cache(
562
710
  async (
563
711
  tenantSlug: string,
564
712
  slugKey: string,
713
+ basePath = "",
565
714
  rawContent?: string,
566
715
  toc?: TocItem[]
567
716
  ) => {
@@ -585,6 +734,7 @@ export const getDocPageContent = cache(
585
734
 
586
735
  return await getRenderedPageData({
587
736
  artifacts,
737
+ basePath,
588
738
  currentPath,
589
739
  rawContent,
590
740
  relativePath: entry.relativePath,
@@ -0,0 +1,82 @@
1
+ export const LLMS_FULL_CHAR_LIMIT = 80_000;
2
+
3
+ const FENCE_LINE_REGEX = /^ {0,3}(`{3,}|~{3,})/;
4
+
5
+ const getTruncationFooter = (indexUrl: string) =>
6
+ [
7
+ "",
8
+ "---",
9
+ "",
10
+ "Content truncated to keep this response under crawler size limits.",
11
+ `Use ${indexUrl} for the full page index and fetch individual .md pages for uncapped content.`,
12
+ ].join("\n");
13
+
14
+ const findOpenFence = (source: string) => {
15
+ let openFence: string | null = null;
16
+
17
+ for (const line of source.split(/\r?\n/)) {
18
+ const marker = FENCE_LINE_REGEX.exec(line)?.[1];
19
+ if (!marker) {
20
+ continue;
21
+ }
22
+
23
+ if (!openFence) {
24
+ openFence = marker;
25
+ continue;
26
+ }
27
+
28
+ if (marker[0] === openFence[0] && marker.length >= openFence.length) {
29
+ openFence = null;
30
+ }
31
+ }
32
+
33
+ return openFence;
34
+ };
35
+
36
+ const closeOpenFence = (source: string) => {
37
+ const openFence = findOpenFence(source);
38
+ return openFence ? `${source}\n${openFence}` : source;
39
+ };
40
+
41
+ const findTruncationBoundary = (source: string, minimum: number) => {
42
+ const sectionBoundary = source.lastIndexOf("\n# ");
43
+ if (sectionBoundary > minimum) {
44
+ return sectionBoundary;
45
+ }
46
+
47
+ const paragraphBoundary = source.lastIndexOf("\n\n");
48
+ if (paragraphBoundary > minimum) {
49
+ return paragraphBoundary;
50
+ }
51
+
52
+ const lineBoundary = source.lastIndexOf("\n");
53
+ return lineBoundary > 0 ? lineBoundary : source.length;
54
+ };
55
+
56
+ export const capLlmsFullContent = (
57
+ content: string,
58
+ indexUrl: string,
59
+ limit = LLMS_FULL_CHAR_LIMIT
60
+ ) => {
61
+ const trimmed = content.trim();
62
+ if (trimmed.length <= limit) {
63
+ return trimmed;
64
+ }
65
+
66
+ const footer = getTruncationFooter(indexUrl);
67
+ let room = limit - footer.length - 16;
68
+ while (room > 0) {
69
+ const draft = trimmed.slice(0, room);
70
+ const boundary = findTruncationBoundary(draft, Math.floor(room / 2));
71
+ const capped = closeOpenFence(draft.slice(0, boundary).trimEnd());
72
+ const result = `${capped}${footer}`;
73
+
74
+ if (result.length <= limit) {
75
+ return result;
76
+ }
77
+
78
+ room -= result.length - limit + 16;
79
+ }
80
+
81
+ return footer.trim().slice(0, limit);
82
+ };
@@ -0,0 +1,79 @@
1
+ const MARKETING_MARKDOWN: Record<string, string> = {
2
+ "/": `# Blode.md
3
+
4
+ Docs your users love. And their AI understands.
5
+
6
+ Write MDX, commit, push. blode.md ships a fast, beautiful docs site in one git push, wired up for the LLMs your users ask too.
7
+
8
+ ## How it works
9
+
10
+ 1. Install the GitHub app at github.com/apps/blodemd
11
+ 2. Pick a repo and a docs folder
12
+ 3. Push to \`main\`, deployed to \`acme.blode.md\`
13
+
14
+ Or use the CLI:
15
+
16
+ \`\`\`
17
+ npm i -g blodemd
18
+ blodemd login
19
+ blodemd new docs
20
+ blodemd push docs
21
+ \`\`\`
22
+
23
+ ## What you get
24
+
25
+ - **GitHub auto-deploy** — Install once. Every push to your branch deploys in seconds.
26
+ - **Custom domains** — Point a domain, get SSL. Or proxy docs at yourdomain.com/docs.
27
+ - **MDX components** — 30+ components out of the box: callouts, tabs, code groups, API refs.
28
+ - **Search** — Full-text search across every page. No plugin, no config.
29
+ - **Content types** — Docs, blogs, changelogs, and courses in one project, one domain.
30
+ - **API reference** — Point at an OpenAPI spec, ship an interactive API reference.
31
+
32
+ ## Links
33
+
34
+ - [About](https://blode.md/about)
35
+ - [Blog](https://blode.md/blog)
36
+ - [Changelog](https://blode.md/changelog)
37
+ - [Privacy](https://blode.md/privacy)
38
+ - [Terms](https://blode.md/terms)
39
+ - [Security](https://blode.md/security)
40
+ - [Docs](https://blode.md/docs)
41
+ - [GitHub](https://github.com/mblode/blodemd)
42
+ `,
43
+ "/about": `# About Blode.md
44
+
45
+ blode.md is a docs platform that lives in your git repo. Write MDX, push to main, ship docs.
46
+ `,
47
+ "/blog": `# Blog
48
+
49
+ Notes from the team building blode.md.
50
+ `,
51
+ "/changelog": `# Changelog
52
+
53
+ Latest updates to the blode.md platform.
54
+ `,
55
+ "/pricing": `# Pricing
56
+
57
+ Blode.md is currently free for hosted projects and MIT licensed for self-hosting.
58
+ `,
59
+ "/privacy": `# Privacy Policy
60
+
61
+ How blode.md collects, uses, and protects your information.
62
+ `,
63
+ "/security": `# Security
64
+
65
+ Security practices at blode.md.
66
+ `,
67
+ "/terms": `# Terms of Service
68
+
69
+ Terms governing your use of blode.md.
70
+ `,
71
+ };
72
+
73
+ export const getMarketingMarkdown = (pathname: string): string | null => {
74
+ const normalized = pathname.replace(/\/+$/, "") || "/";
75
+ return MARKETING_MARKDOWN[normalized] ?? null;
76
+ };
77
+
78
+ export const hasMarketingMarkdown = (pathname: string): boolean =>
79
+ getMarketingMarkdown(pathname) !== null;
@@ -0,0 +1,31 @@
1
+ import { platformRootDomain } from "./env";
2
+
3
+ export const MARKETING_ORIGIN = `https://${platformRootDomain}`;
4
+
5
+ export const MARKETING_CANONICAL_PATHS = [
6
+ "/",
7
+ "/about",
8
+ "/blog",
9
+ "/changelog",
10
+ "/pricing",
11
+ "/privacy",
12
+ "/security",
13
+ "/terms",
14
+ ] as const;
15
+
16
+ export const MARKETING_API_CATALOG_PATH = "/.well-known/api-catalog";
17
+ export const MARKETING_SKILLS_INDEX_PATH =
18
+ "/.well-known/agent-skills/index.json";
19
+ export const MARKETING_MCP_SERVER_CARD_PATH =
20
+ "/.well-known/mcp/server-card.json";
21
+ export const MARKETING_OAUTH_AUTHORIZATION_SERVER_PATH =
22
+ "/.well-known/oauth-authorization-server";
23
+ export const MARKETING_OAUTH_PROTECTED_RESOURCE_PATH =
24
+ "/.well-known/oauth-protected-resource";
25
+ export const MARKETING_OPENID_CONFIGURATION_PATH =
26
+ "/.well-known/openid-configuration";
27
+ export const MARKETING_API_DOCS_PATH = "/docs/api/overview";
28
+ export const MARKETING_API_STATUS_PATH = "/api/health";
29
+ export const MARKETING_API_SERVICE_DESC_PATH = "/api/openapi.json";
30
+
31
+ export const marketingUrl = (path: string) => `${MARKETING_ORIGIN}${path}`;
package/docs/lib/mdx.ts CHANGED
@@ -8,7 +8,7 @@ import rehypeAutolinkHeadings from "rehype-autolink-headings";
8
8
  import rehypeSlug from "rehype-slug";
9
9
  import remarkGfm from "remark-gfm";
10
10
 
11
- import { mdxComponents } from "@/components/mdx";
11
+ import { createMdxComponents } from "@/components/mdx";
12
12
 
13
13
  import { getHighlighter, SHIKI_THEME_PAIR } from "./shiki";
14
14
 
@@ -25,17 +25,22 @@ const stripFrontmatter = (source: string) => ({
25
25
  * `outputFormat: 'function-body'`. This is the fast path — no parsing,
26
26
  * no plugin execution, no Shiki. Sub-millisecond execution.
27
27
  */
28
- export const renderFromCompiled = async (compiledSource: string) => {
28
+ export const renderFromCompiled = async (
29
+ compiledSource: string,
30
+ basePath = "",
31
+ currentPath = ""
32
+ ) => {
33
+ const components = createMdxComponents(basePath, currentPath);
29
34
  const mdxModule = await run(compiledSource, {
30
35
  ...jsxRuntime,
31
36
  baseUrl: import.meta.url,
32
37
  });
33
38
  const Content = mdxModule.default as ComponentType<{
34
- components?: typeof mdxComponents;
39
+ components?: ReturnType<typeof createMdxComponents>;
35
40
  }>;
36
41
 
37
42
  return {
38
- content: createElement(Content, { components: mdxComponents }),
43
+ content: createElement(Content, { components }),
39
44
  frontmatter: {} as Record<string, unknown>,
40
45
  };
41
46
  };
@@ -44,17 +49,23 @@ export const renderFromCompiled = async (compiledSource: string) => {
44
49
  * Full MDX compilation + rendering. Used as fallback for local development
45
50
  * (FsContentSource) and any content that wasn't pre-compiled at deploy time.
46
51
  */
47
- export const renderMdx = async (source: string) => {
52
+ export const renderMdx = async (
53
+ source: string,
54
+ basePath = "",
55
+ currentPath = ""
56
+ ) => {
48
57
  const { body, frontmatter } = stripFrontmatter(source);
58
+ const components = createMdxComponents(basePath, currentPath);
49
59
  const highlighter = await getHighlighter();
50
60
  const shikiTransformer = rehypeShikiFromHighlighter(highlighter, {
51
61
  defaultColor: false,
62
+ fallbackLanguage: "text",
52
63
  themes: SHIKI_THEME_PAIR,
53
64
  });
54
65
  const shikiPlugin = () => shikiTransformer;
55
66
 
56
67
  const result = await compileMDX({
57
- components: mdxComponents,
68
+ components,
58
69
  options: {
59
70
  blockDangerousJS: false,
60
71
  blockJS: false,
@@ -57,7 +57,19 @@ export const getMarkdownExportSlug = (pathname: string, basePath = "") => {
57
57
  export const isExternalHref = (href: string) =>
58
58
  ABSOLUTE_URL_REGEX.test(href) || href.startsWith("//");
59
59
 
60
- export const resolveHref = (href: string, basePath = "") => {
60
+ const isDotRelativeHref = (href: string) =>
61
+ href.startsWith("./") || href.startsWith("../");
62
+
63
+ const resolveDotRelativePath = (pathPart: string, currentPath: string) => {
64
+ const cleanCurrentPath = normalizePath(currentPath);
65
+ const currentDirectory = cleanCurrentPath.includes("/")
66
+ ? cleanCurrentPath.slice(0, cleanCurrentPath.lastIndexOf("/") + 1)
67
+ : "";
68
+ const resolved = new URL(pathPart, `https://docs.local/${currentDirectory}`);
69
+ return normalizePath(resolved.pathname) || "index";
70
+ };
71
+
72
+ export const resolveHref = (href: string, basePath = "", currentPath = "") => {
61
73
  if (
62
74
  !href ||
63
75
  href.startsWith("#") ||
@@ -70,10 +82,14 @@ export const resolveHref = (href: string, basePath = "") => {
70
82
  const suffixIndex = href.search(/[?#]/);
71
83
  const pathPart = suffixIndex === -1 ? href : href.slice(0, suffixIndex);
72
84
  const suffix = suffixIndex === -1 ? "" : href.slice(suffixIndex);
85
+ const resolvedPath =
86
+ currentPath && isDotRelativeHref(pathPart)
87
+ ? resolveDotRelativePath(pathPart, currentPath)
88
+ : pathPart;
73
89
  const normalizedBase = basePath
74
90
  ? withLeadingSlash(basePath).replaceAll(/\/+$/g, "")
75
91
  : "";
76
- const normalizedPath = withLeadingSlash(pathPart || "/");
92
+ const normalizedPath = withLeadingSlash(resolvedPath || "/");
77
93
 
78
94
  if (
79
95
  normalizedBase &&
@@ -83,5 +99,5 @@ export const resolveHref = (href: string, basePath = "") => {
83
99
  return `${normalizedPath}${suffix}`;
84
100
  }
85
101
 
86
- return `${toDocHref(pathPart || "index", basePath)}${suffix}`;
102
+ return `${toDocHref(resolvedPath || "index", basePath)}${suffix}`;
87
103
  };
package/docs/lib/shiki.ts CHANGED
@@ -12,9 +12,13 @@ const COMMON_LANGS = [
12
12
  "jsx",
13
13
  "markdown",
14
14
  "mdx",
15
+ "nginx",
15
16
  "python",
16
17
  "rust",
18
+ "sh",
19
+ "shell",
17
20
  "sql",
21
+ "text",
18
22
  "tsx",
19
23
  "typescript",
20
24
  "yaml",
@@ -21,6 +21,7 @@ const DEFAULT_RESERVED_PATHS = [
21
21
  "/favicon.ico",
22
22
  "/llms.txt",
23
23
  "/logos",
24
+ "/mcp",
24
25
  "/oauth",
25
26
  "/robots.txt",
26
27
  "/sitemap.xml",
@@ -64,11 +65,23 @@ const BACKSLASH_TO_SLASH_REGEX = /\\/g;
64
65
  const DEFAULT_DOCS_BASE_PATH = "/docs";
65
66
  const TENANT_RESOLUTION_REVALIDATE_SECONDS = 300;
66
67
  const ROOT_TENANT_UTILITY_PATHS = new Set([
68
+ "/.well-known/llms-full.txt",
69
+ "/.well-known/llms.txt",
67
70
  "/llms-full.txt",
68
71
  "/llms.txt",
69
72
  "/robots.txt",
70
73
  "/sitemap.xml",
71
74
  ]);
75
+ const WELL_KNOWN_SKILLS_SEGMENT = "/.well-known/skills/";
76
+ const LLMS_SEGMENT_SEGMENT = "/llms/";
77
+
78
+ export const isTenantUtilityPath = (pathname: string) =>
79
+ ROOT_TENANT_UTILITY_PATHS.has(pathname) ||
80
+ pathname.startsWith(WELL_KNOWN_SKILLS_SEGMENT) ||
81
+ (pathname.startsWith(LLMS_SEGMENT_SEGMENT) && pathname.endsWith(".txt"));
82
+
83
+ const isPathMatch = (pathname: string, prefix: string) =>
84
+ pathname === prefix || pathname.startsWith(`${prefix}/`);
72
85
 
73
86
  const slugifyPath = (value: string) => {
74
87
  const trimmed = value
@@ -134,7 +147,7 @@ export const isReservedPath = (pathname: string) => {
134
147
  if (isRootStaticAsset(pathname)) {
135
148
  return true;
136
149
  }
137
- return DEFAULT_RESERVED_PATHS.some((prefix) => pathname.startsWith(prefix));
150
+ return DEFAULT_RESERVED_PATHS.some((prefix) => isPathMatch(pathname, prefix));
138
151
  };
139
152
 
140
153
  const resolveSubdomainBasePath = (pathname: string): string => {
@@ -157,7 +170,7 @@ const buildTenantPathResolution = (
157
170
  pathname: string,
158
171
  basePath: string
159
172
  ) => {
160
- if (ROOT_TENANT_UTILITY_PATHS.has(pathname)) {
173
+ if (isTenantUtilityPath(pathname)) {
161
174
  return {
162
175
  basePath,
163
176
  host,
@@ -1,4 +1,7 @@
1
+ import type { ProjectAnalytics } from "@repo/contracts";
2
+
1
3
  export const TENANT_HEADERS = {
4
+ ANALYTICS: "x-tenant-analytics",
2
5
  BASE_PATH: "x-tenant-base-path",
3
6
  CUSTOM_DOMAINS: "x-tenant-custom-domains",
4
7
  DEPLOYMENT_ID: "x-tenant-deployment-id",
@@ -12,3 +15,29 @@ export const TENANT_HEADERS = {
12
15
  STRATEGY: "x-tenant-strategy",
13
16
  SUBDOMAIN: "x-tenant-subdomain",
14
17
  } as const;
18
+
19
+ const hasAnyProvider = (value: ProjectAnalytics): boolean =>
20
+ Boolean(value.ga4?.measurementId || value.posthog?.projectKey);
21
+
22
+ export const encodeTenantAnalyticsHeader = (
23
+ analytics: ProjectAnalytics | undefined
24
+ ): string | null => {
25
+ if (!analytics || !hasAnyProvider(analytics)) {
26
+ return null;
27
+ }
28
+ return encodeURIComponent(JSON.stringify(analytics));
29
+ };
30
+
31
+ export const decodeTenantAnalyticsHeader = (
32
+ encoded: string | null | undefined
33
+ ): ProjectAnalytics | null => {
34
+ if (!encoded) {
35
+ return null;
36
+ }
37
+ try {
38
+ const parsed = JSON.parse(decodeURIComponent(encoded)) as ProjectAnalytics;
39
+ return hasAnyProvider(parsed) ? parsed : null;
40
+ } catch {
41
+ return null;
42
+ }
43
+ };