blodemd 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dev-server/lib/local-runtime.tsx +12 -15
- package/dev-server/next.config.js +19 -9
- package/dev-server/package.json +5 -1
- package/dev-server/tsconfig.json +0 -3
- package/dist/cli.mjs +1205 -969
- package/dist/cli.mjs.map +1 -1
- package/docs/app/globals.css +1 -1
- package/docs/components/api/api-playground.tsx +2 -5
- package/docs/components/api/api-reference.tsx +29 -3
- package/docs/components/docs/contextual-menu.tsx +37 -33
- package/docs/components/docs/copy-page-menu.tsx +18 -0
- package/docs/components/docs/doc-header.tsx +4 -1
- package/docs/components/docs/doc-shell.tsx +48 -18
- package/docs/components/docs/doc-sidebar.tsx +131 -89
- package/docs/components/docs/doc-toc.tsx +1 -0
- package/docs/components/docs/mobile-nav.tsx +72 -68
- package/docs/components/docs/sidebar-active-highlight.tsx +34 -0
- package/docs/components/docs/sidebar-scroll-area.tsx +9 -4
- package/docs/components/git/repo-picker.tsx +5 -2
- package/docs/components/mdx/index.tsx +31 -21
- package/docs/components/posthog-provider.tsx +42 -0
- package/docs/components/tenant-analytics.tsx +49 -0
- package/docs/components/third-parties.tsx +4 -1
- package/docs/components/ui/copy-button.tsx +16 -2
- package/docs/components/ui/search-dialog.tsx +431 -0
- package/docs/components/ui/search.tsx +44 -427
- package/docs/components/web-mcp.tsx +115 -0
- package/docs/lib/agent-skills.ts +48 -0
- package/docs/lib/config.ts +4 -5
- package/docs/lib/docs-collection.ts +7 -22
- package/docs/lib/docs-runtime.tsx +160 -10
- package/docs/lib/llms-full.ts +82 -0
- package/docs/lib/marketing-markdown.ts +79 -0
- package/docs/lib/marketing-site.ts +31 -0
- package/docs/lib/mdx.ts +17 -6
- package/docs/lib/routes.ts +19 -3
- package/docs/lib/shiki.ts +4 -0
- package/docs/lib/tenancy.ts +15 -2
- package/docs/lib/tenant-headers.ts +29 -0
- package/docs/lib/tenant-static.ts +73 -99
- package/package.json +10 -5
- package/packages/@repo/common/package.json +3 -3
- package/packages/@repo/contracts/dist/analytics.d.ts +21 -0
- package/packages/@repo/contracts/dist/analytics.d.ts.map +1 -0
- package/packages/@repo/contracts/dist/analytics.js +18 -0
- package/packages/@repo/contracts/dist/deployment.js +1 -1
- package/packages/@repo/contracts/dist/git.js +2 -2
- package/packages/@repo/contracts/dist/ids.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/ids.js +2 -1
- package/packages/@repo/contracts/dist/index.d.ts +1 -0
- package/packages/@repo/contracts/dist/index.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/index.js +1 -0
- package/packages/@repo/contracts/dist/project.d.ts +18 -1
- package/packages/@repo/contracts/dist/project.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/project.js +3 -1
- package/packages/@repo/contracts/dist/tenant.d.ts +36 -0
- package/packages/@repo/contracts/dist/tenant.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/tenant.js +2 -0
- package/packages/@repo/contracts/package.json +3 -3
- package/packages/@repo/contracts/src/analytics.ts +33 -0
- package/packages/@repo/contracts/src/deployment.ts +1 -1
- package/packages/@repo/contracts/src/git.ts +2 -2
- package/packages/@repo/contracts/src/ids.ts +2 -1
- package/packages/@repo/contracts/src/index.ts +1 -0
- package/packages/@repo/contracts/src/project.ts +3 -1
- package/packages/@repo/contracts/src/tenant.ts +2 -0
- package/packages/@repo/models/dist/docs-config.d.ts +0 -198
- package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
- package/packages/@repo/models/dist/docs-config.js +3 -2
- package/packages/@repo/models/dist/tenant.d.ts +12 -0
- package/packages/@repo/models/dist/tenant.d.ts.map +1 -1
- package/packages/@repo/models/package.json +3 -3
- package/packages/@repo/models/src/docs-config.ts +3 -3
- package/packages/@repo/models/src/tenant.ts +15 -0
- package/packages/@repo/prebuild/dist/openapi.d.ts.map +1 -1
- package/packages/@repo/prebuild/dist/openapi.js +32 -4
- package/packages/@repo/prebuild/package.json +3 -3
- package/packages/@repo/prebuild/src/openapi.ts +42 -4
- package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/blob-source.js +18 -5
- package/packages/@repo/previewing/dist/config-loader.d.ts +134 -0
- package/packages/@repo/previewing/dist/config-loader.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/config-loader.js +231 -0
- package/packages/@repo/previewing/dist/constants.d.ts +15 -0
- package/packages/@repo/previewing/dist/constants.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/constants.js +14 -0
- package/packages/@repo/previewing/dist/content-index.d.ts +7 -0
- package/packages/@repo/previewing/dist/content-index.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/content-index.js +291 -0
- package/packages/@repo/previewing/dist/index.d.ts +13 -121
- package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/index.js +11 -1003
- package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts +3 -0
- package/packages/@repo/previewing/dist/markdown/agent-markdown.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/markdown/agent-markdown.js +176 -0
- package/packages/@repo/previewing/dist/markdown/format.d.ts +8 -0
- package/packages/@repo/previewing/dist/markdown/format.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/markdown/format.js +43 -0
- package/packages/@repo/previewing/dist/markdown/links.d.ts +3 -0
- package/packages/@repo/previewing/dist/markdown/links.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/markdown/links.js +31 -0
- package/packages/@repo/previewing/dist/openapi-pages.d.ts +7 -0
- package/packages/@repo/previewing/dist/openapi-pages.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/openapi-pages.js +200 -0
- package/packages/@repo/previewing/dist/search-index.d.ts +4 -0
- package/packages/@repo/previewing/dist/search-index.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/search-index.js +23 -0
- package/packages/@repo/previewing/dist/serialization.d.ts +13 -0
- package/packages/@repo/previewing/dist/serialization.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/serialization.js +108 -0
- package/packages/@repo/previewing/dist/toc-index.d.ts +5 -0
- package/packages/@repo/previewing/dist/toc-index.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/toc-index.js +53 -0
- package/packages/@repo/previewing/dist/types.d.ts +92 -0
- package/packages/@repo/previewing/dist/types.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/types.js +1 -0
- package/packages/@repo/previewing/dist/utility-index.d.ts +7 -0
- package/packages/@repo/previewing/dist/utility-index.d.ts.map +1 -0
- package/packages/@repo/previewing/dist/utility-index.js +201 -0
- package/packages/@repo/previewing/package.json +3 -3
- package/packages/@repo/previewing/src/blob-source.ts +20 -5
- package/packages/@repo/previewing/src/config-loader.ts +316 -0
- package/packages/@repo/previewing/src/constants.ts +16 -0
- package/packages/@repo/previewing/src/content-index.ts +384 -0
- package/packages/@repo/previewing/src/index.ts +69 -1515
- package/packages/@repo/previewing/src/markdown/agent-markdown.ts +254 -0
- package/packages/@repo/previewing/src/markdown/format.ts +63 -0
- package/packages/@repo/previewing/src/markdown/links.ts +46 -0
- package/packages/@repo/previewing/src/openapi-pages.ts +319 -0
- package/packages/@repo/previewing/src/search-index.ts +38 -0
- package/packages/@repo/previewing/src/serialization.ts +191 -0
- package/packages/@repo/previewing/src/toc-index.ts +72 -0
- package/packages/@repo/previewing/src/types.ts +105 -0
- package/packages/@repo/previewing/src/utility-index.ts +304 -0
- package/packages/@repo/validation/package.json +3 -3
- package/scripts/repo-packages.mjs +8 -0
- package/docs/components/ui/site-footer.tsx +0 -41
|
@@ -1,22 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
380
|
+
: await artifacts.contentSource.readFile(relativePath));
|
|
350
381
|
const rendered = compiled
|
|
351
|
-
? await renderFromCompiled(
|
|
352
|
-
|
|
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
|
|
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 {
|
|
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 (
|
|
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
|
|
39
|
+
components?: ReturnType<typeof createMdxComponents>;
|
|
35
40
|
}>;
|
|
36
41
|
|
|
37
42
|
return {
|
|
38
|
-
content: createElement(Content, { components
|
|
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 (
|
|
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
|
|
68
|
+
components,
|
|
58
69
|
options: {
|
|
59
70
|
blockDangerousJS: false,
|
|
60
71
|
blockJS: false,
|
package/docs/lib/routes.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
102
|
+
return `${toDocHref(resolvedPath || "index", basePath)}${suffix}`;
|
|
87
103
|
};
|
package/docs/lib/shiki.ts
CHANGED
package/docs/lib/tenancy.ts
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
+
};
|