blume 0.6.7 → 0.8.0

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 (211) hide show
  1. package/CHANGELOG.md +618 -0
  2. package/LICENSE +21 -0
  3. package/README.md +107 -0
  4. package/dist/cli/index.js +2609 -1041
  5. package/dist/cli/index.js.map +110 -103
  6. package/dist/types/ai/component-markdown.d.ts +34 -0
  7. package/dist/types/components/content/youtube.d.ts +18 -0
  8. package/dist/types/core/base-path.d.ts +47 -0
  9. package/dist/types/core/config-input.d.ts +110 -12
  10. package/dist/types/core/config.d.ts +6 -4
  11. package/dist/types/core/data.d.ts +4 -0
  12. package/dist/types/core/i18n-ui.d.ts +477 -135
  13. package/dist/types/core/schema.d.ts +309 -195
  14. package/dist/types/core/sources/types.d.ts +2 -0
  15. package/dist/types/core/types.d.ts +6 -1
  16. package/dist/types/index.d.ts +1 -0
  17. package/dist/types/openapi/references.d.ts +60 -0
  18. package/docs/01-quickstart.mdx +5 -2
  19. package/docs/02-deployment.mdx +24 -9
  20. package/docs/03-faq.mdx +46 -16
  21. package/docs/advanced/custom-pages.mdx +1 -1
  22. package/docs/advanced/skills.mdx +1 -1
  23. package/docs/configuration/ai.mdx +49 -10
  24. package/docs/configuration/customization.mdx +11 -0
  25. package/docs/configuration/index.mdx +33 -3
  26. package/docs/configuration/seo.mdx +2 -2
  27. package/docs/content/components.mdx +30 -3
  28. package/docs/content/i18n.mdx +1 -1
  29. package/docs/content/islands.mdx +8 -0
  30. package/docs/content/navigation.mdx +3 -3
  31. package/docs/content/sources.mdx +1 -1
  32. package/docs/content/syntax.mdx +17 -2
  33. package/docs/index.mdx +2 -2
  34. package/docs/reference/cli.mdx +8 -6
  35. package/package.json +15 -4
  36. package/skills/blume/SKILL.md +5 -3
  37. package/skills/blume-update-docs/SKILL.md +3 -2
  38. package/src/ai/agent-readability.ts +11 -5
  39. package/src/ai/ask-context.ts +7 -2
  40. package/src/ai/ask-data.ts +3 -0
  41. package/src/ai/ask.ts +12 -7
  42. package/src/ai/component-markdown.ts +461 -0
  43. package/src/ai/llms.ts +143 -23
  44. package/src/ai/markdown.ts +35 -6
  45. package/src/ai/mcp/data.ts +33 -8
  46. package/src/ai/mcp/discovery.ts +10 -3
  47. package/src/ai/mcp/server.ts +24 -7
  48. package/src/ai/visibility.ts +74 -0
  49. package/src/astro/component-slots.ts +16 -4
  50. package/src/astro/examples.ts +12 -7
  51. package/src/astro/generate.ts +393 -189
  52. package/src/astro/index.ts +5 -1
  53. package/src/astro/integration.ts +9 -5
  54. package/src/astro/islands.ts +11 -5
  55. package/src/astro/markdown-negotiation.ts +2 -2
  56. package/src/astro/pages.ts +89 -22
  57. package/src/astro/templates.ts +259 -25
  58. package/src/blume-modules.d.ts +8 -0
  59. package/src/cli/commands/build.ts +131 -38
  60. package/src/cli/commands/check.ts +1 -1
  61. package/src/cli/commands/dev.ts +71 -17
  62. package/src/cli/commands/doctor.ts +2 -2
  63. package/src/cli/commands/eject.ts +47 -19
  64. package/src/cli/commands/init.ts +120 -180
  65. package/src/cli/commands/preview.ts +4 -1
  66. package/src/cli/commands/validate.ts +44 -2
  67. package/src/cli/dev-lock.ts +34 -19
  68. package/src/cli/eject-scripts.ts +72 -0
  69. package/src/cli/env.ts +15 -5
  70. package/src/cli/init/questions.ts +158 -0
  71. package/src/cli/init/scaffold.ts +380 -0
  72. package/src/cli/required-secrets.ts +2 -1
  73. package/src/components/content/AccordionItem.astro +23 -4
  74. package/src/components/content/Badge.astro +3 -1
  75. package/src/components/content/Card.astro +4 -2
  76. package/src/components/content/CodeBlock.astro +3 -0
  77. package/src/components/content/Component.astro +30 -16
  78. package/src/components/content/Diff.astro +3 -1
  79. package/src/components/content/Step.astro +10 -1
  80. package/src/components/content/Tabs.astro +15 -3
  81. package/src/components/content/Tile.astro +2 -1
  82. package/src/components/content/Tooltip.astro +3 -1
  83. package/src/components/content/Update.astro +9 -2
  84. package/src/components/content/auto-type-table.ts +25 -9
  85. package/src/components/content/base-href.ts +33 -0
  86. package/src/components/content/changelog-element.ts +9 -2
  87. package/src/components/content/diff.ts +12 -6
  88. package/src/components/content/mermaid-element.ts +10 -2
  89. package/src/components/index.ts +23 -1
  90. package/src/components/islands/AskAI.astro +5 -2
  91. package/src/components/islands/ask-ai.tsx +68 -12
  92. package/src/components/islands/base-path.ts +28 -0
  93. package/src/components/islands/hooks.ts +44 -9
  94. package/src/components/layout/Banner.astro +12 -3
  95. package/src/components/layout/Breadcrumbs.astro +2 -1
  96. package/src/components/layout/Favicon.astro +3 -2
  97. package/src/components/layout/Header.astro +15 -5
  98. package/src/components/layout/LanguageSwitcher.astro +2 -1
  99. package/src/components/layout/Logo.astro +13 -4
  100. package/src/components/layout/NavSelector.astro +2 -1
  101. package/src/components/layout/NavTree.astro +22 -7
  102. package/src/components/layout/PageActions.astro +25 -10
  103. package/src/components/layout/PageFeedback.astro +4 -1
  104. package/src/components/layout/PageLayout.astro +51 -9
  105. package/src/components/layout/Pagination.astro +3 -2
  106. package/src/components/layout/ReferenceLayout.astro +8 -1
  107. package/src/components/layout/RootLayout.astro +74 -13
  108. package/src/components/layout/Search.astro +107 -27
  109. package/src/components/layout/nav-utils.ts +18 -10
  110. package/src/components/layout/search/algolia.ts +11 -2
  111. package/src/components/layout/search/endpoint.ts +11 -5
  112. package/src/components/layout/search/orama-cloud.ts +8 -2
  113. package/src/components/layout/search/pagefind.ts +3 -0
  114. package/src/components/layout/search/types.ts +5 -1
  115. package/src/components/layout/search/typesense.ts +4 -1
  116. package/src/components/layout/toc-element.ts +8 -2
  117. package/src/components/openapi/ApiTagOperations.astro +2 -1
  118. package/src/components/openapi/Operation.astro +47 -40
  119. package/src/components/openapi/RequestPanel.astro +8 -2
  120. package/src/components/openapi/helpers.ts +71 -3
  121. package/src/components/openapi/panel.ts +1 -1
  122. package/src/components/openapi/snippets.ts +25 -11
  123. package/src/core/base-path.ts +94 -0
  124. package/src/core/builtin-tags.ts +2 -0
  125. package/src/core/component-overrides.ts +103 -74
  126. package/src/core/config-input.ts +118 -17
  127. package/src/core/config.ts +8 -5
  128. package/src/core/content.ts +2 -0
  129. package/src/core/data.ts +4 -0
  130. package/src/core/diagnostics.ts +54 -34
  131. package/src/core/gitignore.ts +4 -1
  132. package/src/core/graph.ts +166 -88
  133. package/src/core/i18n-ui.ts +63 -3
  134. package/src/core/last-modified.ts +15 -6
  135. package/src/core/links.ts +69 -25
  136. package/src/core/manifest.ts +62 -45
  137. package/src/core/nav-diagnostics.ts +1 -1
  138. package/src/core/navigation.ts +144 -58
  139. package/src/core/package-json.ts +17 -2
  140. package/src/core/project-graph.ts +25 -15
  141. package/src/core/schema.ts +605 -620
  142. package/src/core/sources/assets.ts +6 -1
  143. package/src/core/sources/filesystem.ts +4 -0
  144. package/src/core/sources/github-releases.ts +2 -1
  145. package/src/core/sources/mdx-remote.ts +76 -63
  146. package/src/core/sources/normalize.ts +236 -91
  147. package/src/core/sources/notion.ts +27 -18
  148. package/src/core/sources/types.ts +2 -0
  149. package/src/core/tsconfig-aliases.ts +59 -30
  150. package/src/core/types.ts +6 -1
  151. package/src/core/ui-packs/ar.ts +1 -0
  152. package/src/core/ui-packs/bg.ts +1 -0
  153. package/src/core/ui-packs/bn.ts +1 -0
  154. package/src/core/ui-packs/ca.ts +1 -0
  155. package/src/core/ui-packs/cs.ts +1 -0
  156. package/src/core/ui-packs/da.ts +1 -0
  157. package/src/core/ui-packs/de.ts +1 -0
  158. package/src/core/ui-packs/el.ts +1 -0
  159. package/src/core/ui-packs/es.ts +1 -0
  160. package/src/core/ui-packs/fa.ts +1 -0
  161. package/src/core/ui-packs/fi.ts +1 -0
  162. package/src/core/ui-packs/fr.ts +2 -1
  163. package/src/core/ui-packs/he.ts +1 -0
  164. package/src/core/ui-packs/hi.ts +1 -0
  165. package/src/core/ui-packs/hr.ts +1 -0
  166. package/src/core/ui-packs/hu.ts +1 -0
  167. package/src/core/ui-packs/id.ts +1 -0
  168. package/src/core/ui-packs/it.ts +1 -0
  169. package/src/core/ui-packs/ja.ts +1 -0
  170. package/src/core/ui-packs/ko.ts +1 -0
  171. package/src/core/ui-packs/nl.ts +1 -0
  172. package/src/core/ui-packs/no.ts +1 -0
  173. package/src/core/ui-packs/pl.ts +1 -0
  174. package/src/core/ui-packs/pt-br.ts +1 -0
  175. package/src/core/ui-packs/pt.ts +1 -0
  176. package/src/core/ui-packs/ro.ts +1 -0
  177. package/src/core/ui-packs/ru.ts +1 -0
  178. package/src/core/ui-packs/sk.ts +1 -0
  179. package/src/core/ui-packs/sr.ts +1 -0
  180. package/src/core/ui-packs/sv.ts +1 -0
  181. package/src/core/ui-packs/th.ts +1 -0
  182. package/src/core/ui-packs/tr.ts +1 -0
  183. package/src/core/ui-packs/uk.ts +1 -0
  184. package/src/core/ui-packs/vi.ts +1 -0
  185. package/src/core/ui-packs/zh-tw.ts +1 -0
  186. package/src/core/ui-packs/zh.ts +1 -0
  187. package/src/deploy/adapter-output.ts +18 -8
  188. package/src/deploy/redirects.ts +25 -2
  189. package/src/deploy/robots.ts +6 -1
  190. package/src/deploy/rss.ts +10 -3
  191. package/src/deploy/sitemap.ts +59 -13
  192. package/src/index.ts +5 -0
  193. package/src/markdown/base-links.ts +60 -0
  194. package/src/markdown/code-title.ts +11 -14
  195. package/src/markdown/index.ts +46 -9
  196. package/src/markdown/inline-code.ts +14 -4
  197. package/src/markdown/package-commands.ts +10 -4
  198. package/src/markdown/themes.ts +24 -0
  199. package/src/openapi/model.ts +15 -5
  200. package/src/openapi/parse.ts +21 -0
  201. package/src/openapi/references.ts +75 -21
  202. package/src/openapi/render-mdx.ts +11 -6
  203. package/src/openapi/scalar.ts +32 -16
  204. package/src/openapi/source.ts +59 -10
  205. package/src/registry/eject.ts +247 -19
  206. package/src/registry/registry.ts +0 -3
  207. package/src/search/build.ts +3 -0
  208. package/src/search/documents.ts +36 -4
  209. package/src/search/sync/typesense.ts +6 -4
  210. package/src/seo/jsonld.ts +28 -17
  211. package/src/theme/entry.ts +85 -20
@@ -1,5 +1,6 @@
1
1
  import type { z } from "zod";
2
2
 
3
+ import type { ComponentMarkdown } from "../ai/component-markdown.ts";
3
4
  import type { FontSlug } from "../theme/fonts.ts";
4
5
  import type {
5
6
  blumeConfigSchema,
@@ -315,11 +316,16 @@ export interface NavSelectorItem {
315
316
  * A header dropdown for switching context — versions, languages, products, or a
316
317
  * generic dropdown. `kind` drives the icon and a11y labeling.
317
318
  */
319
+ /** Context-partition selector kinds (a versioned/localized/multi-product site). */
320
+ type NavSelectorContextKind = "product" | "version";
321
+ /** What a header selector switches between. */
322
+ type NavSelectorKind = "dropdown" | "language" | NavSelectorContextKind;
323
+
318
324
  export interface NavSelector {
319
325
  /** The options shown in the dropdown. */
320
326
  items?: NavSelectorItem[];
321
327
  /** What the selector switches between. */
322
- kind: "dropdown" | "language" | "product" | "version";
328
+ kind: NavSelectorKind;
323
329
  /** Selector label / current value. */
324
330
  label: string;
325
331
  }
@@ -385,6 +391,11 @@ export interface FontsConfig {
385
391
  }
386
392
 
387
393
  /** Colors, fonts, radius, and color-mode behavior. */
394
+ /** Corner radius scale (`none`/`sm` tighter, `md`/`lg` rounder). */
395
+ type RadiusScaleTight = "none" | "sm";
396
+ type RadiusScaleRound = "md" | "lg";
397
+ type RadiusScale = RadiusScaleTight | RadiusScaleRound;
398
+
388
399
  export interface ThemeConfig {
389
400
  /**
390
401
  * Accent color. A palette name (`blue`, `violet`, `green`, …) or any CSS
@@ -405,7 +416,7 @@ export interface ThemeConfig {
405
416
  /** Initial color mode. Defaults to `system`. */
406
417
  mode?: "system" | "light" | "dark";
407
418
  /** Corner radius scale. Defaults to `md`. */
408
- radius?: "none" | "sm" | "md" | "lg";
419
+ radius?: RadiusScale;
409
420
  }
410
421
 
411
422
  // ---------------------------------------------------------------------------
@@ -477,6 +488,10 @@ export interface AskSuggestion {
477
488
  }
478
489
 
479
490
  /** The Ask AI chat assistant. */
491
+ /** Backends that can route an Ask AI request. */
492
+ type AskProviderGateway = "gateway" | "openrouter" | "llmgateway";
493
+ type AskProvider = AskProviderGateway | "inkeep" | "openai-compatible";
494
+
480
495
  export interface AskConfig {
481
496
  /**
482
497
  * Name of the env var holding the provider API key. Each provider has a
@@ -493,22 +508,51 @@ export interface AskConfig {
493
508
  /** Model id to use. Defaults to `openai/gpt-5.5`. */
494
509
  model?: string;
495
510
  /** Which backend routes the request. Defaults to `gateway`. */
496
- provider?:
497
- | "gateway"
498
- | "openrouter"
499
- | "llmgateway"
500
- | "inkeep"
501
- | "openai-compatible";
511
+ provider?: AskProvider;
502
512
  /** Starter prompts shown before the first question. */
503
513
  suggestions?: AskSuggestion[];
504
514
  }
505
515
 
516
+ /** What the `llms.txt`/`llms-full.txt` files include. */
517
+ export interface LlmsTxtConfig {
518
+ /** Emit `llms.txt` and `llms-full.txt`. Defaults to `true`. */
519
+ enabled?: boolean;
520
+ /**
521
+ * Include the generated API reference pages (OpenAPI/AsyncAPI). Defaults to
522
+ * `true`; set `false` to keep a placeholder or example spec's pages out of
523
+ * the LLM-facing files.
524
+ */
525
+ openapi?: boolean;
526
+ }
527
+
506
528
  /** AI-facing features: the Ask AI assistant and an `llms.txt` manifest. */
507
529
  export interface AiConfig {
508
530
  /** The Ask AI chat assistant. */
509
531
  ask?: AskConfig;
510
- /** Emit `llms.txt` (an index of the docs for LLMs). Defaults to `true`. */
511
- llmsTxt?: boolean;
532
+ /**
533
+ * Emit `llms.txt` (an index of the docs for LLMs). Defaults to `true`.
534
+ * The object form adds knobs for what the files include.
535
+ */
536
+ llmsTxt?: boolean | LlmsTxtConfig;
537
+ /**
538
+ * Markdown serializers for custom components in agent-facing output (the
539
+ * `.md` mirror, `llms-full.txt`, MCP `get_page`), keyed by JSX name. Each
540
+ * receives the component's statically-evaluated `props` and downleveled
541
+ * `children` and returns replacement Markdown — or `null` to leave the JSX
542
+ * verbatim. A same-name entry replaces a built-in serializer.
543
+ *
544
+ * These live in `blume.config.ts` (which is executed at build time), not in
545
+ * `components.tsx` (which is only statically analyzed, never run).
546
+ *
547
+ * ```ts
548
+ * ai: {
549
+ * markdownComponents: {
550
+ * Chart: ({ props }) => `![${props.title}](/charts/${props.slug}.png)`,
551
+ * },
552
+ * }
553
+ * ```
554
+ */
555
+ markdownComponents?: Record<string, ComponentMarkdown>;
512
556
  }
513
557
 
514
558
  // ---------------------------------------------------------------------------
@@ -603,9 +647,13 @@ export interface I18nConfig {
603
647
  * Where and how the site deploys. `site` (and `adapter`) are auto-detected from
604
648
  * the platform env on Vercel, Netlify, and Cloudflare.
605
649
  */
650
+ /** Astro server-output adapters, by hosting platform. */
651
+ type CloudDeploymentAdapter = "netlify" | "cloudflare";
652
+ type DeploymentAdapter = "vercel" | "node" | CloudDeploymentAdapter;
653
+
606
654
  export interface DeploymentConfig {
607
655
  /** Astro adapter for server output. `null` (default) keeps a static build. */
608
- adapter?: "vercel" | "node" | "netlify" | "cloudflare" | null;
656
+ adapter?: DeploymentAdapter | null;
609
657
  /** Base path when the site is served from a subdirectory. */
610
658
  base?: string;
611
659
  /** Build output mode. Defaults to `static`. */
@@ -617,12 +665,17 @@ export interface DeploymentConfig {
617
665
  site?: string;
618
666
  }
619
667
 
668
+ /** HTTP redirect status codes: permanent (301/308) and temporary (302/307). */
669
+ type RedirectStatusPermanent = 301 | 308;
670
+ type RedirectStatusTemporary = 302 | 307;
671
+ type RedirectStatus = RedirectStatusPermanent | RedirectStatusTemporary;
672
+
620
673
  /** A URL redirect rule. */
621
674
  export interface RedirectConfig {
622
675
  /** Path to redirect from. */
623
676
  from: string;
624
677
  /** HTTP status. Defaults to `301`. */
625
- status?: 301 | 302 | 307 | 308;
678
+ status?: RedirectStatus;
626
679
  /** Path or URL to redirect to. */
627
680
  to: string;
628
681
  }
@@ -717,7 +770,10 @@ export interface CodeConfig {
717
770
  export interface MarkdownConfig {
718
771
  /** Code-block rendering: language icons, line wrap. */
719
772
  code?: CodeConfig;
720
- /** Syntax-highlighting themes for fenced code blocks. */
773
+ /**
774
+ * Syntax-highlighting themes for every code surface — fenced blocks, inline
775
+ * `` `code`{:lang} ``, `<CodeBlock>`, and `<Diff>`.
776
+ */
721
777
  codeBlocks?: {
722
778
  /** Shiki theme names per color mode. */
723
779
  theme?: {
@@ -736,6 +792,16 @@ export interface MarkdownConfig {
736
792
  imageZoom?: boolean;
737
793
  }
738
794
 
795
+ /** React island behavior. */
796
+ export interface ReactConfig {
797
+ /**
798
+ * Auto-memoize React components/hooks with the React Compiler
799
+ * (`babel-plugin-react-compiler`). On by default whenever React is enabled;
800
+ * set to `false` to skip the compiler's babel pass. Defaults to `true`.
801
+ */
802
+ compiler?: boolean;
803
+ }
804
+
739
805
  // ---------------------------------------------------------------------------
740
806
  // OpenAPI / AsyncAPI
741
807
  // ---------------------------------------------------------------------------
@@ -787,6 +853,25 @@ export interface AsyncApiConfig {
787
853
  // Misc top-level unions
788
854
  // ---------------------------------------------------------------------------
789
855
 
856
+ /** `<Component />` example previews (the object form of `examples`). */
857
+ export interface ExamplesConfig {
858
+ /**
859
+ * A stylesheet, relative to the project root, injected into every preview
860
+ * frame after Blume's default tokens. Previews render inside an isolated
861
+ * iframe the docs styles never reach, so design tokens for the previewed
862
+ * components — shadcn variables, `@theme` mappings, custom fonts — live
863
+ * here. Tailwind is already provided in the frame; the file should hold
864
+ * tokens and styles, not another `@import "tailwindcss"`.
865
+ */
866
+ css?: string;
867
+ /**
868
+ * Where example files live, relative to the project root. Defaults to
869
+ * `examples`; may be a glob to target a registry that colocates component
870
+ * sources with their examples (e.g. `registry/<pkg>/**\/examples/*`).
871
+ */
872
+ source?: string;
873
+ }
874
+
790
875
  /**
791
876
  * Reader-facing "Export" page actions. A boolean toggles both formats; the
792
877
  * object form enables each individually. Defaults to `false`.
@@ -843,6 +928,16 @@ export interface BlumeConfig {
843
928
  asyncapi?: AsyncApiConfig;
844
929
  /** Site-wide announcement banner shown above the header. */
845
930
  banner?: BannerConfig;
931
+ /**
932
+ * Site-wide mount point prepended to every generated route (e.g. `/docs`) —
933
+ * pages, links, redirects, sitemap, OG images, `llms.txt`, and the search
934
+ * index — while staying invisible to the sidebar/nav tree (no wrapper group).
935
+ * Distinct from a per-source `prefix` (which namespaces one source *and*
936
+ * creates a group) and from `deployment.base` (Astro's host-subdirectory
937
+ * base, for serving the whole site — root included — from a subpath). The two
938
+ * compose: with both set, a page lands at `{deployment.base}/{basePath}/page`.
939
+ */
940
+ basePath?: string;
846
941
  /** Where content lives and how it's discovered. */
847
942
  content?: ContentConfig;
848
943
  /** Where and how the site deploys (site URL, adapter, output mode). */
@@ -850,11 +945,15 @@ export interface BlumeConfig {
850
945
  /** Default meta description, used where a page sets none. */
851
946
  description?: string;
852
947
  /**
853
- * Where `<Component path>` resolves live previews and their source from,
854
- * relative to the project root. Defaults to `examples`. May be a glob to
855
- * target a registry that colocates component sources with their examples.
948
+ * `<Component path>` example previews. A string is shorthand for
949
+ * `{ source }`: where examples live, relative to the project root (defaults
950
+ * to `examples`; may be a glob to target a registry that colocates
951
+ * component sources with their examples). The object form adds `css` — a
952
+ * stylesheet injected into every preview frame (previews render in an
953
+ * iframe the docs theme never reaches), for the previewed components'
954
+ * design tokens, e.g. shadcn variables.
856
955
  */
857
- examples?: string;
956
+ examples?: string | ExamplesConfig;
858
957
  /** Reader-facing PDF/EPUB export actions. Defaults to `false`. */
859
958
  export?: ExportConfig;
860
959
  /** Show the per-page "Was this helpful?" widget. Defaults to `true`. */
@@ -875,6 +974,8 @@ export interface BlumeConfig {
875
974
  navigation?: NavigationConfig;
876
975
  /** Native OpenAPI reference. */
877
976
  openapi?: OpenApiConfig;
977
+ /** React island behavior (compiler auto-memoization). */
978
+ react?: ReactConfig;
878
979
  /** URL redirect rules. */
879
980
  redirects?: RedirectConfig[];
880
981
  /** Search backend and credentials. */
@@ -73,8 +73,9 @@ import type { Diagnostic } from "./types.ts";
73
73
  * - `search` — search backend `provider` (`orama` by default; `pagefind`,
74
74
  * `algolia`, `typesense`, `orama-cloud`, `mixedbread`, or `none`) plus its
75
75
  * credential block.
76
- * - `ai` — `ask` (the Ask AI chat endpoint and its provider/model) and `llmsTxt`
77
- * (emit `llms.txt`).
76
+ * - `ai` — `ask` (the Ask AI chat endpoint and its provider/model), `llmsTxt`
77
+ * (emit `llms.txt`), and `markdownComponents` (Markdown serializers for
78
+ * custom components in agent-facing output).
78
79
  * - `mcp` — expose the docs as an MCP server for connecting agents.
79
80
  *
80
81
  * **SEO, feeds & analytics**
@@ -91,8 +92,9 @@ import type { Diagnostic } from "./types.ts";
91
92
  * - `i18n` — opt-in multi-locale: `locales`, `defaultLocale`, `parser`
92
93
  * (`dir` vs filename `dot` suffix), and per-locale UI overrides.
93
94
  *
94
- * - `examples` — where `<Component path>` previews resolve their source from
95
- * (defaults to `examples/`; supports a glob for colocated registries).
95
+ * - `examples` — `<Component path>` previews: `source` (default `examples/`;
96
+ * supports a glob for colocated registries) and `css`, a stylesheet injected
97
+ * into the isolated preview frames (e.g. shadcn variables).
96
98
  *
97
99
  * @example Zero-config — just render the Markdown under `docs/`.
98
100
  * ```ts
@@ -197,11 +199,12 @@ export const loadConfig = async (
197
199
  };
198
200
  // Surface every issue in one failing run — reporting only the first turns
199
201
  // a three-mistake config into three fix-rerun-fail loops.
202
+ const moreIssues = rest.map((d) => ` - ${d.message}`).join("\n");
200
203
  throw new BlumeError(
201
204
  rest.length > 0
202
205
  ? {
203
206
  ...primary,
204
- message: `${primary.message}\n${rest.length} more config issue(s):\n${rest.map((d) => ` - ${d.message}`).join("\n")}`,
207
+ message: `${primary.message}\n${rest.length} more config issue(s):\n${moreIssues}`,
205
208
  }
206
209
  : primary
207
210
  );
@@ -22,6 +22,7 @@ export const discoverContent = async (options: {
22
22
  include: string[];
23
23
  exclude: string[];
24
24
  defaultType: string;
25
+ basePath?: string;
25
26
  i18n?: ResolvedI18nConfig;
26
27
  }): Promise<{ pages: PageRecord[]; diagnostics: Diagnostic[] }> => {
27
28
  const source = filesystemSource({
@@ -38,6 +39,7 @@ export const discoverContent = async (options: {
38
39
 
39
40
  for (const entry of entries) {
40
41
  const normalized = normalizeEntry(entry, {
42
+ basePath: options.basePath ?? "",
41
43
  defaultType: options.defaultType,
42
44
  i18n: options.i18n,
43
45
  source: { name: source.name, prefix: source.prefix, staged: false },
package/src/core/data.ts CHANGED
@@ -93,6 +93,10 @@ export interface BlumeDataConfig {
93
93
  suggestions: NonNullable<ResolvedConfig["ai"]["ask"]>["suggestions"];
94
94
  } | null;
95
95
  banner: BlumeBanner | null;
96
+ /** Site-wide route mount point, normalized to `""` or `/seg` (see config). */
97
+ basePath: string;
98
+ /** `markdown.codeBlocks.theme`: light/dark Shiki themes for code surfaces. */
99
+ codeThemes: ResolvedConfig["markdown"]["codeBlocks"]["theme"];
96
100
  /** `markdown.code.wrap`: wrap long code lines instead of scrolling. */
97
101
  codeWrap: boolean;
98
102
  description: string | undefined;
@@ -20,35 +20,40 @@ export const createDiagnostic = (diagnostic: Diagnostic): Diagnostic =>
20
20
  /** Docs site base; diagnostic help links resolve against it. */
21
21
  const DOCS_BASE = "https://useblume.dev";
22
22
 
23
+ const DOCS_DEPLOYMENT = "/docs/deployment";
24
+ const DOCS_REFERENCE_CLI = "/docs/reference/cli";
25
+ const DOCS_CONTENT_SOURCES = "/docs/content/sources";
26
+ const DOCS_CONTENT_NAVIGATION = "/docs/content/navigation";
27
+
23
28
  /** Diagnostic code → the docs page that explains it. */
24
29
  const DOCS_PATHS: Record<string, string> = {
25
- BLUME_ADAPTER_REQUIRED: "/docs/deployment",
26
- BLUME_ASSETS_UNCHECKED: "/docs/reference/cli",
27
- BLUME_ASSET_FETCH_FAILED: "/docs/content/sources",
28
- BLUME_BROKEN_ANCHOR: "/docs/reference/cli",
29
- BLUME_BROKEN_ASSET: "/docs/reference/cli",
30
- BLUME_BROKEN_LINK: "/docs/reference/cli",
30
+ BLUME_ADAPTER_REQUIRED: DOCS_DEPLOYMENT,
31
+ BLUME_ASSETS_UNCHECKED: DOCS_REFERENCE_CLI,
32
+ BLUME_ASSET_FETCH_FAILED: DOCS_CONTENT_SOURCES,
33
+ BLUME_BROKEN_ANCHOR: DOCS_REFERENCE_CLI,
34
+ BLUME_BROKEN_ASSET: DOCS_REFERENCE_CLI,
35
+ BLUME_BROKEN_LINK: DOCS_REFERENCE_CLI,
31
36
  BLUME_CONFIG_INVALID: "/docs/configuration",
32
37
  BLUME_CONFIG_LOAD_FAILED: "/docs/configuration",
33
- BLUME_CONTENT_ROOT_MISSING: "/docs/content/sources",
34
- BLUME_DEAD_LINK: "/docs/reference/cli",
35
- BLUME_DUPLICATE_ROUTE: "/docs/content/navigation",
38
+ BLUME_CONTENT_ROOT_MISSING: DOCS_CONTENT_SOURCES,
39
+ BLUME_DEAD_LINK: DOCS_REFERENCE_CLI,
40
+ BLUME_DUPLICATE_ROUTE: DOCS_CONTENT_NAVIGATION,
36
41
  BLUME_FRONTMATTER_INVALID: "/docs/reference/frontmatter",
37
42
  BLUME_META_INVALID: "/docs/content/meta",
38
43
  BLUME_META_LOAD_FAILED: "/docs/content/meta",
39
- BLUME_MISSING_SECRET: "/docs/deployment",
40
- BLUME_NAV_DUPLICATE_LABEL: "/docs/content/navigation",
41
- BLUME_NAV_HIDDEN_IN_SIDEBAR: "/docs/content/navigation",
42
- BLUME_NAV_MISSING_PAGE: "/docs/content/navigation",
44
+ BLUME_MISSING_SECRET: DOCS_DEPLOYMENT,
45
+ BLUME_NAV_DUPLICATE_LABEL: DOCS_CONTENT_NAVIGATION,
46
+ BLUME_NAV_HIDDEN_IN_SIDEBAR: DOCS_CONTENT_NAVIGATION,
47
+ BLUME_NAV_MISSING_PAGE: DOCS_CONTENT_NAVIGATION,
43
48
  BLUME_NODE_VERSION: "/docs/quickstart",
44
- BLUME_SERVER_FEATURE_REQUIRED: "/docs/deployment",
45
- BLUME_SOURCE_FETCH_FAILED: "/docs/content/sources",
46
- BLUME_SOURCE_MISCONFIGURED: "/docs/content/sources",
47
- BLUME_SOURCE_OFFLINE: "/docs/content/sources",
48
- BLUME_SOURCE_SDK_MISSING: "/docs/content/sources",
49
- BLUME_SOURCE_UNAVAILABLE: "/docs/content/sources",
49
+ BLUME_SERVER_FEATURE_REQUIRED: DOCS_DEPLOYMENT,
50
+ BLUME_SOURCE_FETCH_FAILED: DOCS_CONTENT_SOURCES,
51
+ BLUME_SOURCE_MISCONFIGURED: DOCS_CONTENT_SOURCES,
52
+ BLUME_SOURCE_OFFLINE: DOCS_CONTENT_SOURCES,
53
+ BLUME_SOURCE_SDK_MISSING: DOCS_CONTENT_SOURCES,
54
+ BLUME_SOURCE_UNAVAILABLE: DOCS_CONTENT_SOURCES,
50
55
  BLUME_UNKNOWN_COMPONENT: "/docs/configuration/customization",
51
- BLUME_UNKNOWN_ICON: "/docs/content/navigation",
56
+ BLUME_UNKNOWN_ICON: DOCS_CONTENT_NAVIGATION,
52
57
  };
53
58
 
54
59
  /** The docs URL that explains a diagnostic code, if one is mapped. */
@@ -74,6 +79,29 @@ const escapeRegExp = (value: string): string =>
74
79
  * lands under its parent. Array indices are skipped. Returns 1-based line/column,
75
80
  * or undefined when nothing matches.
76
81
  */
82
+ const stepSegment = (
83
+ source: string,
84
+ segment: string | number,
85
+ cursor: number
86
+ ): { index: number; next: number; stop: boolean } => {
87
+ // A non-string path segment (array index) is skipped without moving on.
88
+ if (typeof segment !== "string") {
89
+ return { index: -1, next: cursor, stop: false };
90
+ }
91
+ // The negative lookbehind keeps a segment like `title` from matching the
92
+ // tail of an unrelated key such as `subtitle:`.
93
+ const matcher = new RegExp(
94
+ `(?<![\\w$])${escapeRegExp(segment)}\\s*[:=]`,
95
+ "gu"
96
+ );
97
+ matcher.lastIndex = cursor;
98
+ const match = matcher.exec(source);
99
+ if (!match) {
100
+ return { index: -1, next: cursor, stop: true };
101
+ }
102
+ return { index: match.index, next: matcher.lastIndex, stop: false };
103
+ };
104
+
77
105
  const locatePath = (
78
106
  source: string,
79
107
  path: readonly (string | number)[]
@@ -81,22 +109,14 @@ const locatePath = (
81
109
  let cursor = 0;
82
110
  let found = -1;
83
111
  for (const segment of path) {
84
- if (typeof segment !== "string") {
85
- continue;
86
- }
87
- // The negative lookbehind keeps a segment like `title` from matching the
88
- // tail of an unrelated key such as `subtitle:`.
89
- const matcher = new RegExp(
90
- `(?<![\\w$])${escapeRegExp(segment)}\\s*[:=]`,
91
- "gu"
92
- );
93
- matcher.lastIndex = cursor;
94
- const match = matcher.exec(source);
95
- if (!match) {
112
+ const step = stepSegment(source, segment, cursor);
113
+ if (step.stop) {
96
114
  break;
97
115
  }
98
- found = match.index;
99
- cursor = matcher.lastIndex;
116
+ cursor = step.next;
117
+ if (step.index >= 0) {
118
+ found = step.index;
119
+ }
100
120
  }
101
121
  if (found < 0) {
102
122
  return;
@@ -18,7 +18,10 @@ export const ensureGitignore = async (
18
18
  const path = join(root, ".gitignore");
19
19
  const existing = existsSync(path) ? await readFile(path, "utf-8") : "";
20
20
  const present = new Set(
21
- existing.split("\n").map(gitignoreKey).filter(Boolean)
21
+ existing.split("\n").flatMap((line) => {
22
+ const key = gitignoreKey(line);
23
+ return key ? [key] : [];
24
+ })
22
25
  );
23
26
  const added = entries.filter((entry) => !present.has(gitignoreKey(entry)));
24
27
  if (added.length === 0) {