blume 0.0.0 → 0.1.1

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 (330) hide show
  1. package/bin/blume.mjs +18 -0
  2. package/dist/cli/index.js +12696 -0
  3. package/dist/cli/index.js.map +145 -0
  4. package/dist/types/core/bridge.d.ts +24 -0
  5. package/dist/types/core/config.d.ts +35 -0
  6. package/dist/types/core/data.d.ts +129 -0
  7. package/dist/types/core/define-components.d.ts +27 -0
  8. package/dist/types/core/define-meta.d.ts +16 -0
  9. package/dist/types/core/deployment-env.d.ts +9 -0
  10. package/dist/types/core/diagnostics.d.ts +17 -0
  11. package/dist/types/core/i18n-ui.d.ts +500 -0
  12. package/dist/types/core/load-module.d.ts +7 -0
  13. package/dist/types/core/package-root.d.ts +17 -0
  14. package/dist/types/core/project.d.ts +9 -0
  15. package/dist/types/core/schema.d.ts +3451 -0
  16. package/dist/types/core/sources/types.d.ts +107 -0
  17. package/dist/types/core/types.d.ts +245 -0
  18. package/dist/types/core/ui-packs/ar.d.ts +3 -0
  19. package/dist/types/core/ui-packs/bg.d.ts +3 -0
  20. package/dist/types/core/ui-packs/bn.d.ts +3 -0
  21. package/dist/types/core/ui-packs/ca.d.ts +3 -0
  22. package/dist/types/core/ui-packs/cs.d.ts +3 -0
  23. package/dist/types/core/ui-packs/da.d.ts +3 -0
  24. package/dist/types/core/ui-packs/de.d.ts +3 -0
  25. package/dist/types/core/ui-packs/el.d.ts +3 -0
  26. package/dist/types/core/ui-packs/es.d.ts +3 -0
  27. package/dist/types/core/ui-packs/fa.d.ts +3 -0
  28. package/dist/types/core/ui-packs/fi.d.ts +3 -0
  29. package/dist/types/core/ui-packs/fr.d.ts +3 -0
  30. package/dist/types/core/ui-packs/he.d.ts +3 -0
  31. package/dist/types/core/ui-packs/hi.d.ts +3 -0
  32. package/dist/types/core/ui-packs/hr.d.ts +3 -0
  33. package/dist/types/core/ui-packs/hu.d.ts +3 -0
  34. package/dist/types/core/ui-packs/id.d.ts +3 -0
  35. package/dist/types/core/ui-packs/index.d.ts +13 -0
  36. package/dist/types/core/ui-packs/it.d.ts +3 -0
  37. package/dist/types/core/ui-packs/ja.d.ts +3 -0
  38. package/dist/types/core/ui-packs/ko.d.ts +3 -0
  39. package/dist/types/core/ui-packs/nl.d.ts +3 -0
  40. package/dist/types/core/ui-packs/no.d.ts +3 -0
  41. package/dist/types/core/ui-packs/pl.d.ts +3 -0
  42. package/dist/types/core/ui-packs/pt-br.d.ts +3 -0
  43. package/dist/types/core/ui-packs/pt.d.ts +3 -0
  44. package/dist/types/core/ui-packs/ro.d.ts +3 -0
  45. package/dist/types/core/ui-packs/ru.d.ts +3 -0
  46. package/dist/types/core/ui-packs/sk.d.ts +3 -0
  47. package/dist/types/core/ui-packs/sr.d.ts +3 -0
  48. package/dist/types/core/ui-packs/sv.d.ts +3 -0
  49. package/dist/types/core/ui-packs/th.d.ts +3 -0
  50. package/dist/types/core/ui-packs/tr.d.ts +3 -0
  51. package/dist/types/core/ui-packs/uk.d.ts +3 -0
  52. package/dist/types/core/ui-packs/vi.d.ts +3 -0
  53. package/dist/types/core/ui-packs/zh-tw.d.ts +3 -0
  54. package/dist/types/core/ui-packs/zh.d.ts +3 -0
  55. package/dist/types/core/version.d.ts +8 -0
  56. package/dist/types/index.d.ts +10 -0
  57. package/dist/types/migrate/mintlify/config.d.ts +2 -0
  58. package/dist/types/migrate/mintlify/i18n.d.ts +7 -0
  59. package/dist/types/theme/fonts.d.ts +163 -0
  60. package/docs/01-quickstart.mdx +99 -0
  61. package/docs/02-deployment.mdx +129 -0
  62. package/docs/advanced/api-reference.mdx +114 -0
  63. package/docs/advanced/blog.mdx +121 -0
  64. package/docs/advanced/changelog.mdx +113 -0
  65. package/docs/advanced/custom-pages.mdx +268 -0
  66. package/docs/advanced/meta.ts +7 -0
  67. package/docs/changelog/v0-1-0.mdx +12 -0
  68. package/docs/changelog/v0-2-0.mdx +16 -0
  69. package/docs/configuration/ai.mdx +228 -0
  70. package/docs/configuration/analytics.mdx +98 -0
  71. package/docs/configuration/customization.mdx +91 -0
  72. package/docs/configuration/export.mdx +70 -0
  73. package/docs/configuration/index.mdx +290 -0
  74. package/docs/configuration/meta.ts +15 -0
  75. package/docs/configuration/search.mdx +172 -0
  76. package/docs/configuration/seo.mdx +196 -0
  77. package/docs/configuration/theming.mdx +178 -0
  78. package/docs/content/components.mdx +651 -0
  79. package/docs/content/i18n.mdx +205 -0
  80. package/docs/content/index.mdx +161 -0
  81. package/docs/content/islands.mdx +94 -0
  82. package/docs/content/meta.mdx +119 -0
  83. package/docs/content/meta.ts +15 -0
  84. package/docs/content/navigation.mdx +168 -0
  85. package/docs/content/sources.mdx +216 -0
  86. package/docs/content/syntax.mdx +445 -0
  87. package/docs/index.mdx +112 -0
  88. package/docs/reference/cli.mdx +43 -0
  89. package/docs/reference/frontmatter.mdx +74 -0
  90. package/docs/reference/meta.ts +7 -0
  91. package/package.json +150 -6
  92. package/src/ai/ask.ts +93 -0
  93. package/src/ai/llms.ts +64 -0
  94. package/src/ai/markdown.ts +31 -0
  95. package/src/ai/mcp/data.ts +74 -0
  96. package/src/ai/mcp/discovery.ts +49 -0
  97. package/src/ai/mcp/server.ts +225 -0
  98. package/src/ai/mcp/tools.ts +47 -0
  99. package/src/assets/icon.png +0 -0
  100. package/src/astro/examples.ts +104 -0
  101. package/src/astro/generate.ts +1006 -0
  102. package/src/astro/index.ts +4 -0
  103. package/src/astro/integration.ts +74 -0
  104. package/src/astro/islands.ts +131 -0
  105. package/src/astro/markdown-negotiation.ts +68 -0
  106. package/src/astro/pages.ts +87 -0
  107. package/src/astro/templates.ts +1326 -0
  108. package/src/cli/commands/add.ts +81 -0
  109. package/src/cli/commands/build.ts +103 -0
  110. package/src/cli/commands/dev.ts +108 -0
  111. package/src/cli/commands/doctor.ts +74 -0
  112. package/src/cli/commands/eject.ts +57 -0
  113. package/src/cli/commands/init.ts +98 -0
  114. package/src/cli/commands/migrate.ts +39 -0
  115. package/src/cli/commands/preview.ts +39 -0
  116. package/src/cli/commands/sync.ts +52 -0
  117. package/src/cli/commands/validate.ts +61 -0
  118. package/src/cli/index.ts +35 -0
  119. package/src/cli/log.ts +37 -0
  120. package/src/cli/prepare.ts +80 -0
  121. package/src/components/Icon.astro +99 -0
  122. package/src/components/content/Accordion.astro +8 -0
  123. package/src/components/content/AccordionItem.astro +121 -0
  124. package/src/components/content/AutoTypeTable.astro +51 -0
  125. package/src/components/content/Badge.astro +124 -0
  126. package/src/components/content/Callout.astro +73 -0
  127. package/src/components/content/Card.astro +104 -0
  128. package/src/components/content/CardGroup.astro +14 -0
  129. package/src/components/content/CodeBlock.astro +28 -0
  130. package/src/components/content/CodeGroup.astro +13 -0
  131. package/src/components/content/Color.astro +15 -0
  132. package/src/components/content/ColorItem.astro +87 -0
  133. package/src/components/content/ColorRow.astro +10 -0
  134. package/src/components/content/Column.astro +6 -0
  135. package/src/components/content/Columns.astro +9 -0
  136. package/src/components/content/Component.astro +65 -0
  137. package/src/components/content/Diff.astro +44 -0
  138. package/src/components/content/Expandable.astro +11 -0
  139. package/src/components/content/FileTree.astro +8 -0
  140. package/src/components/content/Frame.astro +70 -0
  141. package/src/components/content/GithubInfo.astro +110 -0
  142. package/src/components/content/Math.astro +24 -0
  143. package/src/components/content/Panel.astro +20 -0
  144. package/src/components/content/Prompt.astro +129 -0
  145. package/src/components/content/Step.astro +34 -0
  146. package/src/components/content/Steps.astro +20 -0
  147. package/src/components/content/Tab.astro +46 -0
  148. package/src/components/content/Tabs.astro +273 -0
  149. package/src/components/content/Tile.astro +42 -0
  150. package/src/components/content/Tooltip.astro +68 -0
  151. package/src/components/content/Tree.astro +300 -0
  152. package/src/components/content/TreeFile.astro +15 -0
  153. package/src/components/content/TreeFolder.astro +62 -0
  154. package/src/components/content/TypeTable.astro +106 -0
  155. package/src/components/content/Update.astro +66 -0
  156. package/src/components/content/Visibility.astro +12 -0
  157. package/src/components/content/Warning.astro +9 -0
  158. package/src/components/content/auto-type-table.ts +141 -0
  159. package/src/components/content/diff.ts +95 -0
  160. package/src/components/content/github-info.ts +79 -0
  161. package/src/components/content/mermaid-element.ts +68 -0
  162. package/src/components/github-mark.ts +9 -0
  163. package/src/components/index.ts +14 -0
  164. package/src/components/islands/AskAI.astro +12 -0
  165. package/src/components/islands/ask-ai.tsx +156 -0
  166. package/src/components/layout/Analytics.astro +63 -0
  167. package/src/components/layout/Banner.astro +50 -0
  168. package/src/components/layout/Breadcrumbs.astro +31 -0
  169. package/src/components/layout/Favicon.astro +22 -0
  170. package/src/components/layout/Fonts.astro +14 -0
  171. package/src/components/layout/Header.astro +188 -0
  172. package/src/components/layout/LanguageSwitcher.astro +56 -0
  173. package/src/components/layout/NavTree.astro +462 -0
  174. package/src/components/layout/PageActions.astro +438 -0
  175. package/src/components/layout/PageFeedback.astro +58 -0
  176. package/src/components/layout/PageLayout.astro +173 -0
  177. package/src/components/layout/Pagination.astro +56 -0
  178. package/src/components/layout/ReferenceLayout.astro +107 -0
  179. package/src/components/layout/RootLayout.astro +537 -0
  180. package/src/components/layout/Search.astro +608 -0
  181. package/src/components/layout/TableOfContents.astro +68 -0
  182. package/src/components/layout/analytics-client.ts +38 -0
  183. package/src/components/layout/head-scripts.ts +19 -0
  184. package/src/components/layout/nav-utils.ts +87 -0
  185. package/src/components/layout/overrides.ts +32 -0
  186. package/src/components/layout/search/algolia.ts +43 -0
  187. package/src/components/layout/search/endpoint.ts +22 -0
  188. package/src/components/layout/search/flexsearch.ts +52 -0
  189. package/src/components/layout/search/orama-cloud.ts +41 -0
  190. package/src/components/layout/search/orama.ts +26 -0
  191. package/src/components/layout/search/pagefind.ts +43 -0
  192. package/src/components/layout/search/types.ts +163 -0
  193. package/src/components/layout/search/typesense.ts +60 -0
  194. package/src/components/layout/toc-element.ts +108 -0
  195. package/src/core/bridge.ts +92 -0
  196. package/src/core/config.ts +112 -0
  197. package/src/core/content.ts +50 -0
  198. package/src/core/data.ts +128 -0
  199. package/src/core/define-components.ts +34 -0
  200. package/src/core/define-meta.ts +20 -0
  201. package/src/core/deployment-env.ts +73 -0
  202. package/src/core/diagnostics.ts +104 -0
  203. package/src/core/frontmatter.ts +43 -0
  204. package/src/core/graph.ts +128 -0
  205. package/src/core/i18n-ui.ts +171 -0
  206. package/src/core/i18n.ts +169 -0
  207. package/src/core/last-modified.ts +88 -0
  208. package/src/core/links.ts +352 -0
  209. package/src/core/load-module.ts +15 -0
  210. package/src/core/manifest.ts +126 -0
  211. package/src/core/meta.ts +97 -0
  212. package/src/core/navigation.ts +392 -0
  213. package/src/core/package-root.ts +37 -0
  214. package/src/core/project-graph.ts +153 -0
  215. package/src/core/project.ts +56 -0
  216. package/src/core/schema.ts +1057 -0
  217. package/src/core/server-features.ts +23 -0
  218. package/src/core/sources/assets.ts +77 -0
  219. package/src/core/sources/cache.ts +122 -0
  220. package/src/core/sources/filesystem.ts +99 -0
  221. package/src/core/sources/mdx-remote.ts +215 -0
  222. package/src/core/sources/mintlify.ts +161 -0
  223. package/src/core/sources/normalize.ts +243 -0
  224. package/src/core/sources/notion.ts +440 -0
  225. package/src/core/sources/portable-text.ts +143 -0
  226. package/src/core/sources/read.ts +36 -0
  227. package/src/core/sources/resolve.ts +158 -0
  228. package/src/core/sources/sanity.ts +218 -0
  229. package/src/core/sources/types.ts +105 -0
  230. package/src/core/tsconfig-aliases.ts +201 -0
  231. package/src/core/types.ts +261 -0
  232. package/src/core/ui-packs/ar.ts +47 -0
  233. package/src/core/ui-packs/bg.ts +47 -0
  234. package/src/core/ui-packs/bn.ts +47 -0
  235. package/src/core/ui-packs/ca.ts +47 -0
  236. package/src/core/ui-packs/cs.ts +47 -0
  237. package/src/core/ui-packs/da.ts +47 -0
  238. package/src/core/ui-packs/de.ts +47 -0
  239. package/src/core/ui-packs/el.ts +47 -0
  240. package/src/core/ui-packs/es.ts +47 -0
  241. package/src/core/ui-packs/fa.ts +47 -0
  242. package/src/core/ui-packs/fi.ts +47 -0
  243. package/src/core/ui-packs/fr.ts +47 -0
  244. package/src/core/ui-packs/he.ts +47 -0
  245. package/src/core/ui-packs/hi.ts +47 -0
  246. package/src/core/ui-packs/hr.ts +47 -0
  247. package/src/core/ui-packs/hu.ts +47 -0
  248. package/src/core/ui-packs/id.ts +47 -0
  249. package/src/core/ui-packs/index.ts +87 -0
  250. package/src/core/ui-packs/it.ts +47 -0
  251. package/src/core/ui-packs/ja.ts +47 -0
  252. package/src/core/ui-packs/ko.ts +47 -0
  253. package/src/core/ui-packs/nl.ts +47 -0
  254. package/src/core/ui-packs/no.ts +47 -0
  255. package/src/core/ui-packs/pl.ts +47 -0
  256. package/src/core/ui-packs/pt-br.ts +47 -0
  257. package/src/core/ui-packs/pt.ts +47 -0
  258. package/src/core/ui-packs/ro.ts +47 -0
  259. package/src/core/ui-packs/ru.ts +47 -0
  260. package/src/core/ui-packs/sk.ts +47 -0
  261. package/src/core/ui-packs/sr.ts +47 -0
  262. package/src/core/ui-packs/sv.ts +47 -0
  263. package/src/core/ui-packs/th.ts +47 -0
  264. package/src/core/ui-packs/tr.ts +47 -0
  265. package/src/core/ui-packs/uk.ts +47 -0
  266. package/src/core/ui-packs/vi.ts +47 -0
  267. package/src/core/ui-packs/zh-tw.ts +47 -0
  268. package/src/core/ui-packs/zh.ts +47 -0
  269. package/src/core/version.ts +23 -0
  270. package/src/deploy/robots.ts +20 -0
  271. package/src/deploy/rss.ts +128 -0
  272. package/src/deploy/sitemap.ts +28 -0
  273. package/src/index.ts +39 -0
  274. package/src/markdown/code-title.ts +71 -0
  275. package/src/markdown/directives.ts +83 -0
  276. package/src/markdown/heading-anchors.ts +137 -0
  277. package/src/markdown/index.ts +228 -0
  278. package/src/markdown/inline-code.ts +108 -0
  279. package/src/markdown/language-icon.ts +172 -0
  280. package/src/markdown/math.ts +32 -0
  281. package/src/markdown/mdast.ts +48 -0
  282. package/src/markdown/mermaid.ts +37 -0
  283. package/src/markdown/package-commands.ts +159 -0
  284. package/src/markdown/package-install.ts +40 -0
  285. package/src/migrate/fumadocs/config.ts +155 -0
  286. package/src/migrate/fumadocs/content.ts +365 -0
  287. package/src/migrate/fumadocs/frontmatter.ts +18 -0
  288. package/src/migrate/fumadocs/groups.ts +230 -0
  289. package/src/migrate/fumadocs/index.ts +337 -0
  290. package/src/migrate/fumadocs/meta.ts +244 -0
  291. package/src/migrate/migrate.ts +53 -0
  292. package/src/migrate/mintlify/config.ts +1040 -0
  293. package/src/migrate/mintlify/content.ts +98 -0
  294. package/src/migrate/mintlify/frontmatter.ts +126 -0
  295. package/src/migrate/mintlify/i18n.ts +51 -0
  296. package/src/migrate/mintlify/icons.ts +128 -0
  297. package/src/migrate/mintlify/index.ts +266 -0
  298. package/src/migrate/mintlify/snippets.ts +306 -0
  299. package/src/migrate/mintlify/transform.ts +80 -0
  300. package/src/migrate/nextra/content.ts +46 -0
  301. package/src/migrate/nextra/frontmatter.ts +40 -0
  302. package/src/migrate/nextra/index.ts +374 -0
  303. package/src/migrate/nextra/meta.ts +266 -0
  304. package/src/migrate/shared.ts +720 -0
  305. package/src/migrate/starlight/config.ts +459 -0
  306. package/src/migrate/starlight/content.ts +78 -0
  307. package/src/migrate/starlight/frontmatter.ts +111 -0
  308. package/src/migrate/starlight/i18n.ts +54 -0
  309. package/src/migrate/starlight/index.ts +131 -0
  310. package/src/og/card.ts +92 -0
  311. package/src/og/index.ts +2 -0
  312. package/src/openapi/scalar.ts +246 -0
  313. package/src/registry/eject.ts +310 -0
  314. package/src/registry/registry.ts +100 -0
  315. package/src/registry/rewrite-imports.ts +39 -0
  316. package/src/runtime/index.ts +14 -0
  317. package/src/search/build.ts +23 -0
  318. package/src/search/documents.ts +164 -0
  319. package/src/search/orama-index.ts +66 -0
  320. package/src/search/providers.ts +91 -0
  321. package/src/search/sync/algolia.ts +30 -0
  322. package/src/search/sync/index.ts +50 -0
  323. package/src/search/sync/orama-cloud.ts +40 -0
  324. package/src/search/sync/typesense.ts +65 -0
  325. package/src/seo/jsonld.ts +113 -0
  326. package/src/theme/entry.ts +637 -0
  327. package/src/theme/fonts.ts +198 -0
  328. package/src/theme/icons.ts +184 -0
  329. package/src/theme/palette.ts +143 -0
  330. package/src/theme/twoslash.ts +81 -0
@@ -0,0 +1,66 @@
1
+ import { create, insertMultiple, search } from "@orama/orama";
2
+ import type { AnyOrama } from "@orama/orama";
3
+
4
+ /**
5
+ * The minimal document shape both the client-side search dialog and the
6
+ * server-side MCP `search_docs` tool index. Mirrors the `blume-search.json`
7
+ * entries built by `buildSearchDocuments`.
8
+ */
9
+ export interface OramaDoc {
10
+ content: string;
11
+ description: string;
12
+ route: string;
13
+ title: string;
14
+ /** Locale code; indexed as an enum so queries can filter to one language. */
15
+ locale?: string;
16
+ /** Carried through for the search dialog's breadcrumb + filter pills. Stored
17
+ * but not indexed, so they ride along on the returned document untouched. */
18
+ breadcrumb?: string[];
19
+ section?: string;
20
+ }
21
+
22
+ const SCHEMA = {
23
+ content: "string",
24
+ description: "string",
25
+ // Enum (not full-text "string") so `where` does an exact-match filter.
26
+ locale: "enum",
27
+ route: "string",
28
+ title: "string",
29
+ } as const;
30
+
31
+ /** Title and description outrank body text, matching the search dialog. */
32
+ const BOOST = { description: 2, title: 3 };
33
+
34
+ /**
35
+ * Build an in-memory Orama full-text index from search documents. Shared by the
36
+ * Orama client loader (browser) and the MCP server (Node), so ranking is
37
+ * identical wherever docs are queried.
38
+ */
39
+ export const buildOramaIndex = async (
40
+ documents: OramaDoc[]
41
+ ): Promise<AnyOrama> => {
42
+ const db = create({ schema: SCHEMA });
43
+ await insertMultiple(db, documents);
44
+ return db;
45
+ };
46
+
47
+ /**
48
+ * Query the index, returning the matching documents (highest-ranked first).
49
+ * When `locale` is given, results are filtered to that language via an exact
50
+ * `where` match on the `locale` enum.
51
+ */
52
+ export const queryOramaIndex = async (
53
+ db: AnyOrama,
54
+ term: string,
55
+ limit: number,
56
+ locale?: string
57
+ ): Promise<OramaDoc[]> => {
58
+ const found = await search(db, {
59
+ boost: BOOST,
60
+ limit,
61
+ properties: ["title", "description", "content"],
62
+ term,
63
+ ...(locale ? { where: { locale: { eq: locale } } } : {}),
64
+ });
65
+ return found.hits.map((hit) => hit.document as unknown as OramaDoc);
66
+ };
@@ -0,0 +1,91 @@
1
+ import type { SearchProvider } from "../core/schema.ts";
2
+
3
+ /**
4
+ * How a search provider integrates with the generated runtime.
5
+ *
6
+ * - `static` providers ship a client-side index (`blume-search.json`) that the
7
+ * browser loads and queries — `orama`, `flexsearch`.
8
+ * - `pagefind` builds its own index from the rendered HTML after the Astro build.
9
+ * - `hosted` providers are queried directly from the browser against an external
10
+ * service, with a build-time sync uploading the index — `algolia`,
11
+ * `orama-cloud`, `typesense`.
12
+ * - `server` providers proxy queries through a generated `/api/search` endpoint
13
+ * that holds a secret key, so they require server output — `mixedbread`.
14
+ */
15
+ export type SearchProviderKind =
16
+ | "static"
17
+ | "pagefind"
18
+ | "hosted"
19
+ | "server"
20
+ | "none";
21
+
22
+ export interface SearchProviderMeta {
23
+ kind: SearchProviderKind;
24
+ /** Extra packages the generated `.blume/package.json` must declare. */
25
+ runtimeDeps: string[];
26
+ /** Whether the provider needs `deployment.output: "server"`. */
27
+ requiresServer: boolean;
28
+ /** Whether a build-time sync uploads the index to an external service. */
29
+ syncs: boolean;
30
+ }
31
+
32
+ export const SEARCH_PROVIDERS: Record<SearchProvider, SearchProviderMeta> = {
33
+ algolia: {
34
+ kind: "hosted",
35
+ requiresServer: false,
36
+ runtimeDeps: ["algoliasearch"],
37
+ syncs: true,
38
+ },
39
+ flexsearch: {
40
+ kind: "static",
41
+ requiresServer: false,
42
+ runtimeDeps: ["flexsearch"],
43
+ syncs: false,
44
+ },
45
+ mixedbread: {
46
+ // Content is synced to the store out-of-band via the `mxbai` CLI, not by an
47
+ // in-process build step.
48
+ kind: "server",
49
+ requiresServer: true,
50
+ runtimeDeps: ["@mixedbread/sdk"],
51
+ syncs: false,
52
+ },
53
+ none: {
54
+ kind: "none",
55
+ requiresServer: false,
56
+ runtimeDeps: [],
57
+ syncs: false,
58
+ },
59
+ orama: {
60
+ kind: "static",
61
+ requiresServer: false,
62
+ runtimeDeps: ["@orama/orama"],
63
+ syncs: false,
64
+ },
65
+ "orama-cloud": {
66
+ kind: "hosted",
67
+ requiresServer: false,
68
+ runtimeDeps: ["@oramacloud/client"],
69
+ syncs: true,
70
+ },
71
+ pagefind: {
72
+ kind: "pagefind",
73
+ requiresServer: false,
74
+ runtimeDeps: [],
75
+ syncs: false,
76
+ },
77
+ typesense: {
78
+ kind: "hosted",
79
+ requiresServer: false,
80
+ runtimeDeps: ["typesense"],
81
+ syncs: true,
82
+ },
83
+ };
84
+
85
+ export const searchProviderMeta = (
86
+ provider: SearchProvider
87
+ ): SearchProviderMeta => SEARCH_PROVIDERS[provider];
88
+
89
+ /** Whether a provider ships a client-loaded `blume-search.json` index. */
90
+ export const servesStaticIndex = (provider: SearchProvider): boolean =>
91
+ SEARCH_PROVIDERS[provider].kind === "static";
@@ -0,0 +1,30 @@
1
+ import type { SearchRecord } from "../documents.ts";
2
+
3
+ export interface AlgoliaSyncConfig {
4
+ appId: string;
5
+ indexName: string;
6
+ }
7
+
8
+ /**
9
+ * Upload the search records to Algolia. Uses the admin key from
10
+ * `ALGOLIA_ADMIN_API_KEY` (never the config, which holds only the public,
11
+ * search-only key). Throws on a missing key/config so the caller can warn.
12
+ */
13
+ export const syncAlgolia = async (
14
+ records: SearchRecord[],
15
+ config: AlgoliaSyncConfig | undefined
16
+ ): Promise<void> => {
17
+ if (!config) {
18
+ throw new Error("search.algolia config is missing.");
19
+ }
20
+ const adminKey = process.env.ALGOLIA_ADMIN_API_KEY;
21
+ if (!adminKey) {
22
+ throw new Error("ALGOLIA_ADMIN_API_KEY is not set.");
23
+ }
24
+ const { algoliasearch } = await import("algoliasearch");
25
+ const client = algoliasearch(config.appId, adminKey);
26
+ await client.saveObjects({
27
+ indexName: config.indexName,
28
+ objects: records.map((record) => ({ ...record, objectID: record._id })),
29
+ });
30
+ };
@@ -0,0 +1,50 @@
1
+ import type { BlumeProject } from "../../core/project-graph.ts";
2
+ import { buildSearchDocuments, toSearchRecords } from "../documents.ts";
3
+ import { searchProviderMeta } from "../providers.ts";
4
+ import { syncAlgolia } from "./algolia.ts";
5
+ import { syncOramaCloud } from "./orama-cloud.ts";
6
+ import { syncTypesense } from "./typesense.ts";
7
+
8
+ /** Minimal logger surface the build command provides. */
9
+ export interface SyncReporter {
10
+ start: (message: string) => void;
11
+ success: (message: string) => void;
12
+ warn: (message: string) => void;
13
+ }
14
+
15
+ /**
16
+ * After a build, upload the per-page records to the configured hosted provider.
17
+ * Reads secret admin keys from the environment; if a key (or config) is
18
+ * missing the sync is skipped with a warning rather than failing the build —
19
+ * CI may sync in a separate step. Providers without an in-process sync
20
+ * (Mixedbread, which uses the `mxbai` CLI) are no-ops here.
21
+ */
22
+ export const syncSearchProvider = async (
23
+ project: BlumeProject,
24
+ reporter: SyncReporter
25
+ ): Promise<void> => {
26
+ const { search } = project.config;
27
+ if (!searchProviderMeta(search.provider).syncs) {
28
+ return;
29
+ }
30
+
31
+ const records = toSearchRecords(await buildSearchDocuments(project));
32
+ reporter.start(`Syncing ${records.length} record(s) to ${search.provider}`);
33
+
34
+ try {
35
+ // The `syncs` guard above admits only these three hosted providers, so an
36
+ // if-chain stays exhaustive without a dead `default` branch.
37
+ if (search.provider === "algolia") {
38
+ await syncAlgolia(records, search.algolia);
39
+ } else if (search.provider === "orama-cloud") {
40
+ await syncOramaCloud(records, search.oramaCloud);
41
+ } else if (search.provider === "typesense") {
42
+ await syncTypesense(records, search.typesense);
43
+ }
44
+ reporter.success(
45
+ `Synced ${records.length} record(s) to ${search.provider}`
46
+ );
47
+ } catch (error) {
48
+ reporter.warn(`Search sync skipped: ${(error as Error).message}`);
49
+ }
50
+ };
@@ -0,0 +1,40 @@
1
+ import type { SearchRecord } from "../documents.ts";
2
+
3
+ export interface OramaCloudSyncConfig {
4
+ indexId?: string;
5
+ }
6
+
7
+ /**
8
+ * Push the search records to an Orama Cloud index via `CloudManager`, using the
9
+ * private key from `ORAMA_PRIVATE_API_KEY`. Snapshots the full record set, then
10
+ * deploys. Throws on a missing key/index so the caller can warn.
11
+ */
12
+ export const syncOramaCloud = async (
13
+ records: SearchRecord[],
14
+ config: OramaCloudSyncConfig | undefined
15
+ ): Promise<void> => {
16
+ if (!config) {
17
+ throw new Error("search.oramaCloud config is missing.");
18
+ }
19
+ if (!config.indexId) {
20
+ throw new Error("search.oramaCloud.indexId is required to sync.");
21
+ }
22
+ const privateKey = process.env.ORAMA_PRIVATE_API_KEY;
23
+ if (!privateKey) {
24
+ throw new Error("ORAMA_PRIVATE_API_KEY is not set.");
25
+ }
26
+ const { CloudManager } = await import("@oramacloud/client");
27
+ const manager = new CloudManager({ api_key: privateKey });
28
+ const index = manager.index(config.indexId);
29
+ await index.snapshot(
30
+ records.map((record) => ({
31
+ content: record.content,
32
+ description: record.description,
33
+ id: record._id,
34
+ tag: record.tag,
35
+ title: record.title,
36
+ url: record.url,
37
+ }))
38
+ );
39
+ await index.deploy();
40
+ };
@@ -0,0 +1,65 @@
1
+ import type { SearchRecord } from "../documents.ts";
2
+
3
+ export interface TypesenseSyncConfig {
4
+ collection: string;
5
+ host: string;
6
+ port?: number;
7
+ protocol?: string;
8
+ }
9
+
10
+ /**
11
+ * Import the search records into a Typesense collection, creating the
12
+ * collection on first run. Uses the admin key from `TYPESENSE_ADMIN_API_KEY`.
13
+ * Throws on a missing key/config so the caller can warn.
14
+ */
15
+ export const syncTypesense = async (
16
+ records: SearchRecord[],
17
+ config: TypesenseSyncConfig | undefined
18
+ ): Promise<void> => {
19
+ if (!config) {
20
+ throw new Error("search.typesense config is missing.");
21
+ }
22
+ const adminKey = process.env.TYPESENSE_ADMIN_API_KEY;
23
+ if (!adminKey) {
24
+ throw new Error("TYPESENSE_ADMIN_API_KEY is not set.");
25
+ }
26
+ const { Client } = await import("typesense");
27
+ const client = new Client({
28
+ apiKey: adminKey,
29
+ nodes: [
30
+ {
31
+ host: config.host,
32
+ port: config.port ?? 443,
33
+ protocol: config.protocol ?? "https",
34
+ },
35
+ ],
36
+ });
37
+
38
+ try {
39
+ await client.collections(config.collection).retrieve();
40
+ } catch {
41
+ await client.collections().create({
42
+ fields: [
43
+ { name: "title", type: "string" },
44
+ { name: "description", optional: true, type: "string" },
45
+ { name: "content", type: "string" },
46
+ { name: "url", type: "string" },
47
+ { facet: true, name: "tag", optional: true, type: "string" },
48
+ ],
49
+ name: config.collection,
50
+ });
51
+ }
52
+
53
+ const documents = records.map((record) => ({
54
+ content: record.content,
55
+ description: record.description,
56
+ id: record._id,
57
+ tag: record.tag,
58
+ title: record.title,
59
+ url: record.url,
60
+ }));
61
+ await client
62
+ .collections(config.collection)
63
+ .documents()
64
+ .import(documents, { action: "upsert" });
65
+ };
@@ -0,0 +1,113 @@
1
+ import type { Crumb } from "../components/layout/nav-utils.ts";
2
+
3
+ /** Inputs for a page's JSON-LD, all known at render time in RootLayout. */
4
+ export interface StructuredDataInput {
5
+ siteName: string;
6
+ /** Absolute site origin, or null when `deployment.site` is unset. */
7
+ siteUrl: string | null;
8
+ title: string;
9
+ description?: string;
10
+ /** Page route, e.g. `/blog/post`. */
11
+ route: string;
12
+ /** Content type — `blog` and `changelog` map to richer article types. */
13
+ pageType?: string;
14
+ /** Publish date (string or YAML Date); emitted as ISO `datePublished`. */
15
+ published?: string | Date | null;
16
+ /** Last-modified date; emitted as ISO `dateModified`. */
17
+ modified?: string | Date | null;
18
+ /** BCP-47 language tag for `inLanguage`; defaults to `en`. */
19
+ locale?: string;
20
+ breadcrumbs: Crumb[];
21
+ }
22
+
23
+ /** schema.org `@type` for each content type; defaults to TechArticle. */
24
+ const ARTICLE_TYPES: Record<string, string> = {
25
+ blog: "BlogPosting",
26
+ changelog: "TechArticle",
27
+ };
28
+
29
+ const trimSlash = (value: string): string => value.replace(/\/$/u, "");
30
+
31
+ const absolute = (base: string | null, path: string): string =>
32
+ base ? `${base}${path}` : path;
33
+
34
+ const toIso = (value: string | Date | null | undefined): string | undefined => {
35
+ if (!value) {
36
+ return;
37
+ }
38
+ const date = new Date(value);
39
+ return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
40
+ };
41
+
42
+ /**
43
+ * Build a schema.org JSON-LD `@graph` for a page: site identity, the page as an
44
+ * article, and its breadcrumb trail. Returns null when there is nothing useful
45
+ * to emit (e.g. the homepage without a configured site). URLs are absolute when
46
+ * `siteUrl` is set, otherwise route-relative.
47
+ */
48
+ export const buildStructuredData = (
49
+ input: StructuredDataInput
50
+ ): Record<string, unknown> | null => {
51
+ const base = input.siteUrl ? trimSlash(input.siteUrl) : null;
52
+ const pageUrl = absolute(base, input.route);
53
+ const graph: Record<string, unknown>[] = [];
54
+
55
+ if (base) {
56
+ graph.push({
57
+ "@id": `${base}#website`,
58
+ "@type": "WebSite",
59
+ name: input.siteName,
60
+ url: base,
61
+ });
62
+ }
63
+
64
+ // The homepage is fully described by the WebSite node; deeper pages get an
65
+ // article node plus a breadcrumb trail.
66
+ if (input.route !== "/") {
67
+ const node: Record<string, unknown> = {
68
+ "@id": `${pageUrl}#page`,
69
+ "@type": ARTICLE_TYPES[input.pageType ?? ""] ?? "TechArticle",
70
+ headline: input.title,
71
+ inLanguage: input.locale || "en",
72
+ name: input.title,
73
+ url: pageUrl,
74
+ };
75
+ if (input.description) {
76
+ node.description = input.description;
77
+ }
78
+ const published = toIso(input.published);
79
+ if (published) {
80
+ node.datePublished = published;
81
+ }
82
+ const modified = toIso(input.modified);
83
+ if (modified) {
84
+ node.dateModified = modified;
85
+ }
86
+ if (base) {
87
+ node.isPartOf = { "@id": `${base}#website` };
88
+ }
89
+ graph.push(node);
90
+
91
+ if (input.breadcrumbs.length > 1) {
92
+ graph.push({
93
+ "@type": "BreadcrumbList",
94
+ itemListElement: input.breadcrumbs.map((crumb, index) => {
95
+ const item: Record<string, unknown> = {
96
+ "@type": "ListItem",
97
+ name: crumb.label,
98
+ position: index + 1,
99
+ };
100
+ if (crumb.route) {
101
+ item.item = absolute(base, crumb.route);
102
+ }
103
+ return item;
104
+ }),
105
+ });
106
+ }
107
+ }
108
+
109
+ if (graph.length === 0) {
110
+ return null;
111
+ }
112
+ return { "@context": "https://schema.org", "@graph": graph };
113
+ };