blume 0.0.0 → 0.1.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 (263) hide show
  1. package/bin/blume.mjs +18 -0
  2. package/dist/cli/index.js +11989 -0
  3. package/dist/cli/index.js.map +141 -0
  4. package/docs/01-quickstart.mdx +99 -0
  5. package/docs/02-deployment.mdx +129 -0
  6. package/docs/advanced/api-reference.mdx +114 -0
  7. package/docs/advanced/blog.mdx +121 -0
  8. package/docs/advanced/changelog.mdx +113 -0
  9. package/docs/advanced/custom-pages.mdx +187 -0
  10. package/docs/advanced/meta.ts +7 -0
  11. package/docs/changelog/v0-1-0.mdx +12 -0
  12. package/docs/changelog/v0-2-0.mdx +16 -0
  13. package/docs/configuration/ai.mdx +228 -0
  14. package/docs/configuration/analytics.mdx +98 -0
  15. package/docs/configuration/customization.mdx +91 -0
  16. package/docs/configuration/export.mdx +70 -0
  17. package/docs/configuration/index.mdx +271 -0
  18. package/docs/configuration/meta.ts +15 -0
  19. package/docs/configuration/search.mdx +172 -0
  20. package/docs/configuration/seo.mdx +196 -0
  21. package/docs/configuration/theming.mdx +178 -0
  22. package/docs/content/components.mdx +565 -0
  23. package/docs/content/i18n.mdx +205 -0
  24. package/docs/content/index.mdx +161 -0
  25. package/docs/content/islands.mdx +94 -0
  26. package/docs/content/meta.mdx +119 -0
  27. package/docs/content/meta.ts +15 -0
  28. package/docs/content/navigation.mdx +168 -0
  29. package/docs/content/sources.mdx +216 -0
  30. package/docs/content/syntax.mdx +445 -0
  31. package/docs/index.mdx +112 -0
  32. package/docs/reference/cli.mdx +43 -0
  33. package/docs/reference/frontmatter.mdx +74 -0
  34. package/docs/reference/meta.ts +7 -0
  35. package/package.json +140 -6
  36. package/src/ai/ask.ts +93 -0
  37. package/src/ai/llms.ts +65 -0
  38. package/src/ai/markdown.ts +31 -0
  39. package/src/ai/mcp/data.ts +74 -0
  40. package/src/ai/mcp/discovery.ts +49 -0
  41. package/src/ai/mcp/server.ts +225 -0
  42. package/src/ai/mcp/tools.ts +47 -0
  43. package/src/assets/icon.png +0 -0
  44. package/src/astro/generate.ts +878 -0
  45. package/src/astro/index.ts +4 -0
  46. package/src/astro/integration.ts +74 -0
  47. package/src/astro/islands.ts +131 -0
  48. package/src/astro/markdown-negotiation.ts +68 -0
  49. package/src/astro/pages.ts +28 -0
  50. package/src/astro/templates.ts +1199 -0
  51. package/src/cli/commands/add.ts +81 -0
  52. package/src/cli/commands/build.ts +103 -0
  53. package/src/cli/commands/dev.ts +108 -0
  54. package/src/cli/commands/doctor.ts +74 -0
  55. package/src/cli/commands/eject.ts +57 -0
  56. package/src/cli/commands/init.ts +98 -0
  57. package/src/cli/commands/migrate.ts +39 -0
  58. package/src/cli/commands/preview.ts +39 -0
  59. package/src/cli/commands/sync.ts +52 -0
  60. package/src/cli/commands/validate.ts +60 -0
  61. package/src/cli/index.ts +35 -0
  62. package/src/cli/log.ts +37 -0
  63. package/src/cli/prepare.ts +80 -0
  64. package/src/components/Icon.astro +99 -0
  65. package/src/components/content/Accordion.astro +8 -0
  66. package/src/components/content/AccordionItem.astro +121 -0
  67. package/src/components/content/AutoTypeTable.astro +51 -0
  68. package/src/components/content/Badge.astro +124 -0
  69. package/src/components/content/Callout.astro +73 -0
  70. package/src/components/content/Card.astro +104 -0
  71. package/src/components/content/CardGroup.astro +14 -0
  72. package/src/components/content/CodeGroup.astro +13 -0
  73. package/src/components/content/Color.astro +15 -0
  74. package/src/components/content/ColorItem.astro +87 -0
  75. package/src/components/content/ColorRow.astro +10 -0
  76. package/src/components/content/Column.astro +6 -0
  77. package/src/components/content/Columns.astro +9 -0
  78. package/src/components/content/Expandable.astro +11 -0
  79. package/src/components/content/FileTree.astro +8 -0
  80. package/src/components/content/Frame.astro +70 -0
  81. package/src/components/content/GithubInfo.astro +110 -0
  82. package/src/components/content/Math.astro +24 -0
  83. package/src/components/content/Panel.astro +20 -0
  84. package/src/components/content/Prompt.astro +129 -0
  85. package/src/components/content/Step.astro +34 -0
  86. package/src/components/content/Steps.astro +20 -0
  87. package/src/components/content/Tab.astro +40 -0
  88. package/src/components/content/Tabs.astro +273 -0
  89. package/src/components/content/Tile.astro +42 -0
  90. package/src/components/content/Tooltip.astro +68 -0
  91. package/src/components/content/Tree.astro +300 -0
  92. package/src/components/content/TreeFile.astro +15 -0
  93. package/src/components/content/TreeFolder.astro +62 -0
  94. package/src/components/content/TypeTable.astro +106 -0
  95. package/src/components/content/Update.astro +66 -0
  96. package/src/components/content/Visibility.astro +12 -0
  97. package/src/components/content/Warning.astro +9 -0
  98. package/src/components/content/auto-type-table.ts +141 -0
  99. package/src/components/content/github-info.ts +79 -0
  100. package/src/components/content/mermaid-element.ts +68 -0
  101. package/src/components/github-mark.ts +9 -0
  102. package/src/components/index.ts +14 -0
  103. package/src/components/islands/AskAI.astro +12 -0
  104. package/src/components/islands/ask-ai.tsx +156 -0
  105. package/src/components/layout/Analytics.astro +63 -0
  106. package/src/components/layout/Banner.astro +50 -0
  107. package/src/components/layout/Breadcrumbs.astro +31 -0
  108. package/src/components/layout/Favicon.astro +15 -0
  109. package/src/components/layout/Fonts.astro +14 -0
  110. package/src/components/layout/Header.astro +188 -0
  111. package/src/components/layout/LanguageSwitcher.astro +56 -0
  112. package/src/components/layout/NavTree.astro +462 -0
  113. package/src/components/layout/PageActions.astro +438 -0
  114. package/src/components/layout/PageFeedback.astro +58 -0
  115. package/src/components/layout/Pagination.astro +56 -0
  116. package/src/components/layout/ReferenceLayout.astro +102 -0
  117. package/src/components/layout/RootLayout.astro +533 -0
  118. package/src/components/layout/Search.astro +608 -0
  119. package/src/components/layout/TableOfContents.astro +68 -0
  120. package/src/components/layout/analytics-client.ts +38 -0
  121. package/src/components/layout/nav-utils.ts +87 -0
  122. package/src/components/layout/overrides.ts +32 -0
  123. package/src/components/layout/search/algolia.ts +43 -0
  124. package/src/components/layout/search/endpoint.ts +22 -0
  125. package/src/components/layout/search/flexsearch.ts +52 -0
  126. package/src/components/layout/search/orama-cloud.ts +41 -0
  127. package/src/components/layout/search/orama.ts +26 -0
  128. package/src/components/layout/search/pagefind.ts +43 -0
  129. package/src/components/layout/search/types.ts +163 -0
  130. package/src/components/layout/search/typesense.ts +60 -0
  131. package/src/components/layout/toc-element.ts +108 -0
  132. package/src/core/bridge.ts +92 -0
  133. package/src/core/config.ts +112 -0
  134. package/src/core/content.ts +50 -0
  135. package/src/core/define-components.ts +34 -0
  136. package/src/core/define-meta.ts +20 -0
  137. package/src/core/deployment-env.ts +73 -0
  138. package/src/core/diagnostics.ts +104 -0
  139. package/src/core/graph.ts +128 -0
  140. package/src/core/i18n-ui.ts +171 -0
  141. package/src/core/i18n.ts +169 -0
  142. package/src/core/last-modified.ts +88 -0
  143. package/src/core/links.ts +336 -0
  144. package/src/core/load-module.ts +15 -0
  145. package/src/core/manifest.ts +126 -0
  146. package/src/core/meta.ts +97 -0
  147. package/src/core/navigation.ts +392 -0
  148. package/src/core/package-root.ts +37 -0
  149. package/src/core/project-graph.ts +153 -0
  150. package/src/core/project.ts +56 -0
  151. package/src/core/schema.ts +1057 -0
  152. package/src/core/server-features.ts +23 -0
  153. package/src/core/sources/assets.ts +77 -0
  154. package/src/core/sources/cache.ts +122 -0
  155. package/src/core/sources/filesystem.ts +99 -0
  156. package/src/core/sources/mdx-remote.ts +216 -0
  157. package/src/core/sources/mintlify.ts +161 -0
  158. package/src/core/sources/normalize.ts +227 -0
  159. package/src/core/sources/notion.ts +440 -0
  160. package/src/core/sources/portable-text.ts +143 -0
  161. package/src/core/sources/read.ts +36 -0
  162. package/src/core/sources/resolve.ts +158 -0
  163. package/src/core/sources/sanity.ts +218 -0
  164. package/src/core/sources/types.ts +105 -0
  165. package/src/core/types.ts +261 -0
  166. package/src/core/ui-packs/ar.ts +47 -0
  167. package/src/core/ui-packs/bg.ts +47 -0
  168. package/src/core/ui-packs/bn.ts +47 -0
  169. package/src/core/ui-packs/ca.ts +47 -0
  170. package/src/core/ui-packs/cs.ts +47 -0
  171. package/src/core/ui-packs/da.ts +47 -0
  172. package/src/core/ui-packs/de.ts +47 -0
  173. package/src/core/ui-packs/el.ts +47 -0
  174. package/src/core/ui-packs/es.ts +47 -0
  175. package/src/core/ui-packs/fa.ts +47 -0
  176. package/src/core/ui-packs/fi.ts +47 -0
  177. package/src/core/ui-packs/fr.ts +47 -0
  178. package/src/core/ui-packs/he.ts +47 -0
  179. package/src/core/ui-packs/hi.ts +47 -0
  180. package/src/core/ui-packs/hr.ts +47 -0
  181. package/src/core/ui-packs/hu.ts +47 -0
  182. package/src/core/ui-packs/id.ts +47 -0
  183. package/src/core/ui-packs/index.ts +87 -0
  184. package/src/core/ui-packs/it.ts +47 -0
  185. package/src/core/ui-packs/ja.ts +47 -0
  186. package/src/core/ui-packs/ko.ts +47 -0
  187. package/src/core/ui-packs/nl.ts +47 -0
  188. package/src/core/ui-packs/no.ts +47 -0
  189. package/src/core/ui-packs/pl.ts +47 -0
  190. package/src/core/ui-packs/pt-br.ts +47 -0
  191. package/src/core/ui-packs/pt.ts +47 -0
  192. package/src/core/ui-packs/ro.ts +47 -0
  193. package/src/core/ui-packs/ru.ts +47 -0
  194. package/src/core/ui-packs/sk.ts +47 -0
  195. package/src/core/ui-packs/sr.ts +47 -0
  196. package/src/core/ui-packs/sv.ts +47 -0
  197. package/src/core/ui-packs/th.ts +47 -0
  198. package/src/core/ui-packs/tr.ts +47 -0
  199. package/src/core/ui-packs/uk.ts +47 -0
  200. package/src/core/ui-packs/vi.ts +47 -0
  201. package/src/core/ui-packs/zh-tw.ts +47 -0
  202. package/src/core/ui-packs/zh.ts +47 -0
  203. package/src/core/version.ts +23 -0
  204. package/src/deploy/robots.ts +20 -0
  205. package/src/deploy/rss.ts +128 -0
  206. package/src/deploy/sitemap.ts +28 -0
  207. package/src/index.ts +27 -0
  208. package/src/markdown/code-title.ts +71 -0
  209. package/src/markdown/directives.ts +83 -0
  210. package/src/markdown/heading-anchors.ts +137 -0
  211. package/src/markdown/index.ts +159 -0
  212. package/src/markdown/inline-code.ts +108 -0
  213. package/src/markdown/language-icon.ts +172 -0
  214. package/src/markdown/math.ts +32 -0
  215. package/src/markdown/mdast.ts +48 -0
  216. package/src/markdown/mermaid.ts +37 -0
  217. package/src/markdown/package-commands.ts +159 -0
  218. package/src/markdown/package-install.ts +40 -0
  219. package/src/migrate/fumadocs/config.ts +106 -0
  220. package/src/migrate/fumadocs/content.ts +365 -0
  221. package/src/migrate/fumadocs/frontmatter.ts +18 -0
  222. package/src/migrate/fumadocs/index.ts +252 -0
  223. package/src/migrate/fumadocs/meta.ts +114 -0
  224. package/src/migrate/migrate.ts +53 -0
  225. package/src/migrate/mintlify/config.ts +1040 -0
  226. package/src/migrate/mintlify/content.ts +98 -0
  227. package/src/migrate/mintlify/frontmatter.ts +126 -0
  228. package/src/migrate/mintlify/i18n.ts +51 -0
  229. package/src/migrate/mintlify/icons.ts +128 -0
  230. package/src/migrate/mintlify/index.ts +266 -0
  231. package/src/migrate/mintlify/snippets.ts +305 -0
  232. package/src/migrate/mintlify/transform.ts +81 -0
  233. package/src/migrate/nextra/content.ts +46 -0
  234. package/src/migrate/nextra/frontmatter.ts +40 -0
  235. package/src/migrate/nextra/index.ts +374 -0
  236. package/src/migrate/nextra/meta.ts +266 -0
  237. package/src/migrate/shared.ts +623 -0
  238. package/src/migrate/starlight/config.ts +459 -0
  239. package/src/migrate/starlight/content.ts +78 -0
  240. package/src/migrate/starlight/frontmatter.ts +111 -0
  241. package/src/migrate/starlight/i18n.ts +54 -0
  242. package/src/migrate/starlight/index.ts +131 -0
  243. package/src/og/card.ts +92 -0
  244. package/src/og/index.ts +2 -0
  245. package/src/openapi/scalar.ts +246 -0
  246. package/src/registry/eject.ts +263 -0
  247. package/src/registry/registry.ts +100 -0
  248. package/src/registry/rewrite-imports.ts +39 -0
  249. package/src/runtime/index.ts +14 -0
  250. package/src/search/build.ts +23 -0
  251. package/src/search/documents.ts +165 -0
  252. package/src/search/orama-index.ts +66 -0
  253. package/src/search/providers.ts +91 -0
  254. package/src/search/sync/algolia.ts +30 -0
  255. package/src/search/sync/index.ts +50 -0
  256. package/src/search/sync/orama-cloud.ts +40 -0
  257. package/src/search/sync/typesense.ts +65 -0
  258. package/src/seo/jsonld.ts +113 -0
  259. package/src/theme/entry.ts +608 -0
  260. package/src/theme/fonts.ts +198 -0
  261. package/src/theme/icons.ts +184 -0
  262. package/src/theme/palette.ts +143 -0
  263. package/src/theme/twoslash.ts +81 -0
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Client behaviour for the `<blume-toc>` custom element wrapping each "On this
3
+ * page" list. As the reader scrolls, it marks the link for the section currently
4
+ * in view with `aria-current="location"` — styled via Tailwind `aria-[current]`
5
+ * variants in RootLayout — giving the table of contents a live scrollspy.
6
+ *
7
+ * The active heading is chosen by rect (the last heading at or above a trigger
8
+ * line just below the sticky header), so it stays correct for sections taller
9
+ * than the viewport and at the bottom of the page. An IntersectionObserver is the
10
+ * cheap primary trigger — it only fires as headings cross the band near the top —
11
+ * and a passive, rAF-throttled scroll listener covers the one case it can't: a
12
+ * final section too short to push its heading past the trigger line.
13
+ *
14
+ * Imported for its side effect (registers the element) from RootLayout's script.
15
+ */
16
+
17
+ interface TocEntry {
18
+ heading: HTMLElement;
19
+ link: HTMLAnchorElement;
20
+ }
21
+
22
+ // Matches the theme's `scroll-padding-top: 4.5rem`, so the highlighted heading
23
+ // agrees with where a clicked anchor lands beneath the sticky header.
24
+ const TRIGGER_OFFSET = 72;
25
+
26
+ class BlumeToc extends HTMLElement {
27
+ #entries: TocEntry[] = [];
28
+ #observer: IntersectionObserver | null = null;
29
+ #current: HTMLAnchorElement | null = null;
30
+ #ticking = false;
31
+
32
+ connectedCallback() {
33
+ for (const link of this.querySelectorAll<HTMLAnchorElement>(
34
+ 'a[href^="#"]'
35
+ )) {
36
+ const id = decodeURIComponent(link.hash.slice(1));
37
+ const heading = id
38
+ ? document.querySelector<HTMLElement>(`#${CSS.escape(id)}`)
39
+ : null;
40
+ if (heading) {
41
+ this.#entries.push({ heading, link });
42
+ }
43
+ }
44
+ if (this.#entries.length === 0) {
45
+ return;
46
+ }
47
+
48
+ this.#observer = new IntersectionObserver(() => this.#update(), {
49
+ rootMargin: `-${TRIGGER_OFFSET}px 0px -70% 0px`,
50
+ threshold: 0,
51
+ });
52
+ for (const { heading } of this.#entries) {
53
+ this.#observer.observe(heading);
54
+ }
55
+ window.addEventListener("scroll", this.#onScroll, { passive: true });
56
+ this.#update();
57
+ }
58
+
59
+ disconnectedCallback() {
60
+ this.#observer?.disconnect();
61
+ this.#observer = null;
62
+ window.removeEventListener("scroll", this.#onScroll);
63
+ }
64
+
65
+ #onScroll = () => {
66
+ if (this.#ticking) {
67
+ return;
68
+ }
69
+ this.#ticking = true;
70
+ requestAnimationFrame(() => {
71
+ this.#ticking = false;
72
+ this.#update();
73
+ });
74
+ };
75
+
76
+ #update() {
77
+ const active = this.#activeLink();
78
+ if (active === this.#current) {
79
+ return;
80
+ }
81
+ this.#current?.removeAttribute("aria-current");
82
+ active?.setAttribute("aria-current", "location");
83
+ this.#current = active;
84
+ }
85
+
86
+ #activeLink(): HTMLAnchorElement | null {
87
+ const scrolledToBottom =
88
+ window.innerHeight + window.scrollY >=
89
+ document.documentElement.scrollHeight - 2;
90
+ if (scrolledToBottom) {
91
+ return this.#entries.at(-1)?.link ?? null;
92
+ }
93
+
94
+ // Headings are in document order, so the last one whose top has reached the
95
+ // trigger line is the section currently being read; default to the first.
96
+ let active = this.#entries[0]?.link ?? null;
97
+ for (const { heading, link } of this.#entries) {
98
+ if (heading.getBoundingClientRect().top <= TRIGGER_OFFSET) {
99
+ active = link;
100
+ }
101
+ }
102
+ return active;
103
+ }
104
+ }
105
+
106
+ if (!customElements.get("blume-toc")) {
107
+ customElements.define("blume-toc", BlumeToc);
108
+ }
@@ -0,0 +1,92 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+
4
+ import { join } from "pathe";
5
+
6
+ import { loadMintlifyConfig } from "../migrate/mintlify/config.ts";
7
+ import { mintlifyI18n } from "../migrate/mintlify/i18n.ts";
8
+ import type { BlumeConfig } from "./schema.ts";
9
+
10
+ /** A detected docs-tool config that Blume can serve without a migration. */
11
+ export interface BridgeDetection {
12
+ /** Which foreign docs tool was detected. */
13
+ tool: "mintlify";
14
+ /** Absolute path of the detected config file (`docs.json`/`mint.json`). */
15
+ configFile: string;
16
+ /** A Blume config synthesized from the foreign config, ready to validate. */
17
+ raw: BlumeConfig;
18
+ }
19
+
20
+ const MINTLIFY_CONFIG_FILES = ["docs.json", "mint.json"];
21
+
22
+ /**
23
+ * Detect a Mintlify project at `root` and synthesize an equivalent Blume config.
24
+ *
25
+ * "Bridge mode" lets a team swap `mintlify dev` for `blume dev` with no file
26
+ * changes: `docs.json` is translated to Blume config (`loadMintlifyConfig`) and
27
+ * its content block is rewired to a single `mintlify` content source, which
28
+ * transforms each MDX page to Blume idiom at scan time. The original
29
+ * `content.root`/`exclude` and `variables` move onto the source (Blume has no
30
+ * runtime variable substitution, so globals are inlined into content there).
31
+ *
32
+ * Returns `null` when no Mintlify config is present. Only called when no
33
+ * `blume.config.*` exists, so an explicit Blume config always takes precedence.
34
+ */
35
+ export const detectMintlifyBridge = async (
36
+ root: string
37
+ ): Promise<BridgeDetection | null> => {
38
+ const configFile = MINTLIFY_CONFIG_FILES.map((name) => join(root, name)).find(
39
+ (candidate) => existsSync(candidate)
40
+ );
41
+ if (!configFile) {
42
+ return null;
43
+ }
44
+
45
+ const config = await loadMintlifyConfig(root, configFile);
46
+
47
+ // i18n is derived from the raw spec (language dirs), mirroring the migrator.
48
+ const spec = JSON.parse(await readFile(configFile, "utf-8")) as Record<
49
+ string,
50
+ unknown
51
+ >;
52
+ const i18n = mintlifyI18n(spec);
53
+ if (i18n) {
54
+ config.i18n = i18n;
55
+ // Language switching is handled by Blume i18n, not a nav selector.
56
+ if (config.navigation?.selectors) {
57
+ config.navigation.selectors = config.navigation.selectors.filter(
58
+ (selector) => selector.kind !== "language"
59
+ );
60
+ }
61
+ }
62
+
63
+ const variables = (config.variables as Record<string, string>) ?? {};
64
+ const root_ = config.content?.root ?? ".";
65
+ const exclude = config.content?.exclude ?? [];
66
+
67
+ return {
68
+ configFile,
69
+ raw: {
70
+ ...config,
71
+ content: {
72
+ // Mirror the excludes onto `content.exclude` too: the generated Astro
73
+ // `docs` collection globs `content.root` (here the project root) and
74
+ // must skip node_modules/snippets just like the source does.
75
+ exclude,
76
+ root: root_,
77
+ sources: [
78
+ {
79
+ configFile,
80
+ exclude,
81
+ root: root_,
82
+ type: "mintlify",
83
+ variables,
84
+ },
85
+ ],
86
+ },
87
+ // Globals are inlined into content by the source; drop the top-level copy.
88
+ variables: undefined,
89
+ },
90
+ tool: "mintlify",
91
+ };
92
+ };
@@ -0,0 +1,112 @@
1
+ import { detectMintlifyBridge } from "./bridge.ts";
2
+ import type { BridgeDetection } from "./bridge.ts";
3
+ import { applyDeploymentEnv } from "./deployment-env.ts";
4
+ import { BlumeError, diagnosticsFromZod } from "./diagnostics.ts";
5
+ import { createModuleLoader } from "./load-module.ts";
6
+ import { findConfigFile } from "./project.ts";
7
+ import { blumeConfigSchema } from "./schema.ts";
8
+ import type { BlumeConfig, ResolvedConfig } from "./schema.ts";
9
+ import type { Diagnostic } from "./types.ts";
10
+
11
+ /**
12
+ * Identity helper for authoring `blume.config.ts`. Exists for type inference
13
+ * and a stable future home for plugin hooks; it does not transform input.
14
+ */
15
+ export const defineConfig = (config: BlumeConfig): BlumeConfig => config;
16
+
17
+ /** Bridge mode info: a foreign docs tool Blume is serving without migrating. */
18
+ export interface ConfigBridge {
19
+ tool: "mintlify";
20
+ /** Absolute path of the foreign config file (`docs.json`/`mint.json`). */
21
+ configFile: string;
22
+ }
23
+
24
+ /** Result of loading + validating a project config. */
25
+ export interface ConfigLoadResult {
26
+ config: ResolvedConfig;
27
+ /** Absolute path of the config file used, or null when defaults were used. */
28
+ configFile: string | null;
29
+ /** Set when a foreign docs config (e.g. Mintlify) is being bridged. */
30
+ bridge: ConfigBridge | null;
31
+ diagnostics: Diagnostic[];
32
+ }
33
+
34
+ const importConfigModule = createModuleLoader();
35
+
36
+ /**
37
+ * Load and validate the project config. When no config file exists, schema
38
+ * defaults produce a fully resolved config so the zero-boilerplate path works.
39
+ */
40
+ export const loadConfig = async (
41
+ root: string,
42
+ /**
43
+ * Supplied only by `blume dev`: the local dev server URL, used as the
44
+ * `deployment.site` fallback when none is configured or detected. Builds
45
+ * never pass it, so production output can't end up pointing at localhost.
46
+ */
47
+ options: { devServerUrl?: string } = {}
48
+ ): Promise<ConfigLoadResult> => {
49
+ const configFile = findConfigFile(root);
50
+
51
+ // With no Blume config, a Mintlify `docs.json` activates bridge mode: serve
52
+ // the unconverted project by synthesizing config + a `mintlify` content source.
53
+ let bridge: BridgeDetection | null = null;
54
+ let raw: unknown = {};
55
+ if (configFile) {
56
+ try {
57
+ raw = await importConfigModule(configFile);
58
+ } catch (error) {
59
+ throw new BlumeError({
60
+ code: "BLUME_CONFIG_LOAD_FAILED",
61
+ file: configFile,
62
+ message: `Failed to load config: ${(error as Error).message}`,
63
+ severity: "error",
64
+ });
65
+ }
66
+ } else {
67
+ bridge = await detectMintlifyBridge(root);
68
+ if (bridge) {
69
+ ({ raw } = bridge);
70
+ }
71
+ }
72
+
73
+ const sourceFile = bridge?.configFile ?? configFile;
74
+ const parsed = blumeConfigSchema.safeParse(raw ?? {});
75
+ if (!parsed.success) {
76
+ const diagnostics = diagnosticsFromZod(parsed.error, {
77
+ code: "BLUME_CONFIG_INVALID",
78
+ file: sourceFile ?? undefined,
79
+ });
80
+ throw new BlumeError(
81
+ diagnostics[0] ?? {
82
+ code: "BLUME_CONFIG_INVALID",
83
+ file: sourceFile ?? undefined,
84
+ message: "Invalid Blume config.",
85
+ severity: "error",
86
+ }
87
+ );
88
+ }
89
+
90
+ // Resolve the canonical site URL, then SEO defaults that depend on it.
91
+ // Precedence: explicit config > platform env (Vercel/Netlify/Cloudflare, via
92
+ // applyDeploymentEnv) > the local dev server URL (dev only).
93
+ const config = applyDeploymentEnv(parsed.data);
94
+ const site = config.deployment.site ?? options.devServerUrl;
95
+
96
+ // OG images need an absolute `og:image`, so they default on once a site URL
97
+ // is known and off otherwise. An explicit `seo.og.enabled` always wins.
98
+ const ogEnabled = config.seo.og.enabled ?? Boolean(site);
99
+
100
+ return {
101
+ bridge: bridge
102
+ ? { configFile: bridge.configFile, tool: bridge.tool }
103
+ : null,
104
+ config: {
105
+ ...config,
106
+ deployment: { ...config.deployment, site },
107
+ seo: { ...config.seo, og: { ...config.seo.og, enabled: ogEnabled } },
108
+ },
109
+ configFile: sourceFile,
110
+ diagnostics: [],
111
+ };
112
+ };
@@ -0,0 +1,50 @@
1
+ import type { ResolvedI18nConfig } from "./schema.ts";
2
+ import { filesystemSource } from "./sources/filesystem.ts";
3
+ import { normalizeEntry } from "./sources/normalize.ts";
4
+ import type { Diagnostic, PageRecord } from "./types.ts";
5
+
6
+ // Re-export the shared parsing helpers from their new home so existing importers
7
+ // (and tests) keep resolving them from `core/content.ts`.
8
+ export {
9
+ extractHeadings,
10
+ extractLinks,
11
+ normalizeEntry,
12
+ slugify,
13
+ } from "./sources/normalize.ts";
14
+
15
+ /**
16
+ * Discover and normalize all content pages under a content root. Thin wrapper
17
+ * around the filesystem `ContentSource` + `normalizeEntry`, kept for callers and
18
+ * tests that scan a single directory directly.
19
+ */
20
+ export const discoverContent = async (options: {
21
+ contentRoot: string;
22
+ include: string[];
23
+ exclude: string[];
24
+ defaultType: string;
25
+ i18n?: ResolvedI18nConfig;
26
+ }): Promise<{ pages: PageRecord[]; diagnostics: Diagnostic[] }> => {
27
+ const source = filesystemSource({
28
+ exclude: options.exclude,
29
+ include: options.include,
30
+ name: "filesystem",
31
+ projectRoot: options.contentRoot,
32
+ root: options.contentRoot,
33
+ });
34
+
35
+ const { entries, diagnostics: loadDiagnostics } = await source.load();
36
+ const pages: PageRecord[] = [];
37
+ const diagnostics: Diagnostic[] = [...loadDiagnostics];
38
+
39
+ for (const entry of entries) {
40
+ const normalized = normalizeEntry(entry, {
41
+ defaultType: options.defaultType,
42
+ i18n: options.i18n,
43
+ source: { name: source.name, prefix: source.prefix, staged: false },
44
+ });
45
+ pages.push(...normalized.pages);
46
+ diagnostics.push(...normalized.diagnostics);
47
+ }
48
+
49
+ return { diagnostics, pages };
50
+ };
@@ -0,0 +1,34 @@
1
+ import type { HydrationMode } from "./schema.ts";
2
+
3
+ /**
4
+ * A reference to a component. Either an imported component (preferred, for type
5
+ * safety) or a string path resolved relative to the project root.
6
+ */
7
+ export type ComponentReference = unknown | string;
8
+
9
+ /** An interactive component plus its hydration strategy. */
10
+ export interface IslandDescriptor {
11
+ component: ComponentReference;
12
+ client: HydrationMode;
13
+ /** Required when `client` is `"media"`. */
14
+ media?: string;
15
+ }
16
+
17
+ /** A component override: a static component or a hydrated island. */
18
+ export type ComponentOverride = ComponentReference | IslandDescriptor;
19
+
20
+ /** User-authored component overrides, grouped by surface. */
21
+ export interface ComponentOverrides {
22
+ /** MDX component map overrides (`Callout`, `Card`, ...). */
23
+ mdx?: Record<string, ComponentOverride>;
24
+ /** Layout slot overrides (`Header`, `Sidebar`, `Search`, ...). */
25
+ layout?: Record<string, ComponentOverride>;
26
+ }
27
+
28
+ /**
29
+ * Identity helper for authoring `components.ts`. Provides type inference and a
30
+ * stable home for future normalization; it does not transform input.
31
+ */
32
+ export const defineComponents = (
33
+ overrides: ComponentOverrides
34
+ ): ComponentOverrides => overrides;
@@ -0,0 +1,20 @@
1
+ import type { FolderMeta } from "./schema.ts";
2
+
3
+ /** A function that computes folder meta, optionally asynchronously. */
4
+ export type FolderMetaFactory = () => FolderMeta | Promise<FolderMeta>;
5
+
6
+ /** What a `meta.ts` may default-export: a plain object or a (async) factory. */
7
+ export type FolderMetaDefinition = FolderMeta | FolderMetaFactory;
8
+
9
+ /**
10
+ * Identity helper for authoring a folder's `meta.ts`. Provides type inference;
11
+ * it does not transform input. Pass an object, or a function (sync or async)
12
+ * that returns one when you need to compute the meta:
13
+ *
14
+ * ```ts
15
+ * export default defineMeta({ title: "Guides", order: 2 });
16
+ * export default defineMeta(async () => ({ pages: await orderPages() }));
17
+ * ```
18
+ */
19
+ export const defineMeta = (meta: FolderMetaDefinition): FolderMetaDefinition =>
20
+ meta;
@@ -0,0 +1,73 @@
1
+ import type { ResolvedConfig } from "./schema.ts";
2
+
3
+ /** The deployment platforms Blume can infer from runtime/CI env vars. */
4
+ type DeploymentAdapter = NonNullable<ResolvedConfig["deployment"]["adapter"]>;
5
+
6
+ interface Platform {
7
+ /** Astro adapter to use when building for server output on this platform. */
8
+ adapter: DeploymentAdapter;
9
+ /** True when the env indicates the build is running on this platform. */
10
+ detect: (env: NodeJS.ProcessEnv) => boolean;
11
+ /** Resolve the canonical site URL from the platform's env vars, or null. */
12
+ site: (env: NodeJS.ProcessEnv) => string | null;
13
+ }
14
+
15
+ /** Prefix a bare host with `https://`; pass values that are already absolute. */
16
+ const toUrl = (value: string | undefined): string | null => {
17
+ const trimmed = value?.trim();
18
+ if (!trimmed) {
19
+ return null;
20
+ }
21
+ return /^https?:\/\//u.test(trimmed) ? trimmed : `https://${trimmed}`;
22
+ };
23
+
24
+ /**
25
+ * Platforms checked in order; the first whose `detect` matches wins. Site URLs
26
+ * prefer the stable production domain over per-deployment preview URLs so the
27
+ * inferred origin (sitemap, OG, RSS) stays put across deploys.
28
+ */
29
+ const PLATFORMS: Platform[] = [
30
+ {
31
+ adapter: "vercel",
32
+ detect: (env) => Boolean(env.VERCEL),
33
+ site: (env) => toUrl(env.VERCEL_PROJECT_PRODUCTION_URL ?? env.VERCEL_URL),
34
+ },
35
+ {
36
+ adapter: "netlify",
37
+ detect: (env) => Boolean(env.NETLIFY),
38
+ site: (env) => toUrl(env.URL ?? env.DEPLOY_PRIME_URL ?? env.DEPLOY_URL),
39
+ },
40
+ {
41
+ adapter: "cloudflare",
42
+ detect: (env) => Boolean(env.CF_PAGES),
43
+ site: (env) => toUrl(env.CF_PAGES_URL),
44
+ },
45
+ ];
46
+
47
+ /**
48
+ * Fill in `deployment.adapter` and `deployment.site` from platform env vars
49
+ * (Vercel, Netlify, Cloudflare Pages) when the user hasn't set them. Explicit
50
+ * config always wins, and the adapter is only inferred for server output (it
51
+ * has no effect on static builds). Mirrors Astro's platform auto-detection so a
52
+ * project deployed to a known host gets a working canonical origin for free.
53
+ */
54
+ export const applyDeploymentEnv = (
55
+ config: ResolvedConfig,
56
+ env: NodeJS.ProcessEnv = process.env
57
+ ): ResolvedConfig => {
58
+ const platform = PLATFORMS.find((candidate) => candidate.detect(env));
59
+ if (!platform) {
60
+ return config;
61
+ }
62
+
63
+ const { deployment } = config;
64
+ const adapter =
65
+ deployment.adapter ??
66
+ (deployment.output === "server" ? platform.adapter : null);
67
+ const site = deployment.site ?? platform.site(env) ?? undefined;
68
+
69
+ if (adapter === deployment.adapter && site === deployment.site) {
70
+ return config;
71
+ }
72
+ return { ...config, deployment: { ...deployment, adapter, site } };
73
+ };
@@ -0,0 +1,104 @@
1
+ import { relative } from "pathe";
2
+ import type { ZodError } from "zod";
3
+
4
+ import type { Diagnostic } from "./types.ts";
5
+
6
+ /** A recoverable error carrying a structured diagnostic. */
7
+ export class BlumeError extends Error {
8
+ readonly diagnostic: Diagnostic;
9
+
10
+ constructor(diagnostic: Diagnostic) {
11
+ super(diagnostic.message);
12
+ this.name = "BlumeError";
13
+ this.diagnostic = diagnostic;
14
+ }
15
+ }
16
+
17
+ export const createDiagnostic = (diagnostic: Diagnostic): Diagnostic =>
18
+ diagnostic;
19
+
20
+ /** Convert a ZodError into Blume diagnostics, anchored to a file. */
21
+ export const diagnosticsFromZod = (
22
+ error: ZodError,
23
+ options: { code: string; file?: string }
24
+ ): Diagnostic[] =>
25
+ error.issues.map((issue) => {
26
+ const schemaPath = issue.path.join(".");
27
+ const received =
28
+ "received" in issue
29
+ ? ` (received: ${JSON.stringify(issue.received)})`
30
+ : "";
31
+ return {
32
+ code: options.code,
33
+ file: options.file,
34
+ message: schemaPath
35
+ ? `${schemaPath}: ${issue.message}${received}`
36
+ : `${issue.message}${received}`,
37
+ schemaPath: schemaPath || undefined,
38
+ severity: "error",
39
+ } satisfies Diagnostic;
40
+ });
41
+
42
+ const ESC = String.fromCodePoint(27);
43
+ const COLORS = {
44
+ blue: `${ESC}[34m`,
45
+ bold: `${ESC}[1m`,
46
+ cyan: `${ESC}[36m`,
47
+ dim: `${ESC}[2m`,
48
+ red: `${ESC}[31m`,
49
+ reset: `${ESC}[0m`,
50
+ yellow: `${ESC}[33m`,
51
+ };
52
+
53
+ const severityColor = (severity: Diagnostic["severity"]): string => {
54
+ if (severity === "error") {
55
+ return COLORS.red;
56
+ }
57
+ if (severity === "warning") {
58
+ return COLORS.yellow;
59
+ }
60
+ return COLORS.blue;
61
+ };
62
+
63
+ /** Format a single diagnostic for terminal output. */
64
+ export const formatDiagnostic = (
65
+ diagnostic: Diagnostic,
66
+ root?: string
67
+ ): string => {
68
+ const color = severityColor(diagnostic.severity);
69
+ const lines: string[] = [
70
+ `${color}${COLORS.bold}${diagnostic.code}${COLORS.reset} ${diagnostic.message}`,
71
+ ];
72
+
73
+ if (diagnostic.file) {
74
+ const location = root ? relative(root, diagnostic.file) : diagnostic.file;
75
+ const column =
76
+ diagnostic.column === undefined ? "" : `:${diagnostic.column}`;
77
+ const position =
78
+ diagnostic.line === undefined ? "" : `:${diagnostic.line}${column}`;
79
+ lines.push(` ${COLORS.dim}at ${location}${position}${COLORS.reset}`);
80
+ }
81
+
82
+ if (diagnostic.suggestion) {
83
+ lines.push(` ${COLORS.cyan}fix: ${diagnostic.suggestion}${COLORS.reset}`);
84
+ }
85
+
86
+ if (diagnostic.docsUrl) {
87
+ lines.push(` ${COLORS.dim}docs: ${diagnostic.docsUrl}${COLORS.reset}`);
88
+ }
89
+
90
+ return lines.join("\n");
91
+ };
92
+
93
+ export const hasErrors = (diagnostics: Diagnostic[]): boolean =>
94
+ diagnostics.some((d) => d.severity === "error");
95
+
96
+ export const countBySeverity = (
97
+ diagnostics: Diagnostic[]
98
+ ): Record<Diagnostic["severity"], number> => {
99
+ const counts = { error: 0, info: 0, warning: 0 };
100
+ for (const diagnostic of diagnostics) {
101
+ counts[diagnostic.severity] += 1;
102
+ }
103
+ return counts;
104
+ };