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,4 @@
1
+ export { generateRuntime } from "./generate.ts";
2
+ export type { GenerateResult } from "./generate.ts";
3
+ export { blumeIntegration } from "./integration.ts";
4
+ export type { BlumeIntegrationOptions, BlumePageRoute } from "./integration.ts";
@@ -0,0 +1,74 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+
3
+ import type { AstroIntegration } from "astro";
4
+
5
+ import { markdownVariantUrl, prefersMarkdown } from "./markdown-negotiation.ts";
6
+
7
+ /** A user page mounted into the generated runtime. */
8
+ export interface BlumePageRoute {
9
+ /** Route pattern, e.g. `/changelog` or `/examples/[slug]`. */
10
+ pattern: string;
11
+ /** Absolute path to the user's `.astro` page file. */
12
+ entrypoint: string;
13
+ }
14
+
15
+ export interface BlumeIntegrationOptions {
16
+ pages: BlumePageRoute[];
17
+ /** Page routes that have a raw-Markdown variant (the content manifest). */
18
+ contentRoutes: string[];
19
+ }
20
+
21
+ /**
22
+ * Dev-server content negotiation: when a client asks for `text/markdown`,
23
+ * transparently rewrite a content-page request to its `.md` variant so the
24
+ * existing raw-Markdown endpoint serves it. Runs only under `blume dev`; static
25
+ * and server builds expose the same content at the `.md` URL. Only routes with
26
+ * a Markdown variant are rewritten, so landing pages and user `.astro` pages
27
+ * keep serving HTML.
28
+ */
29
+ const negotiateMarkdown =
30
+ (routes: ReadonlySet<string>) =>
31
+ (req: IncomingMessage, res: ServerResponse, next: () => void): void => {
32
+ if (
33
+ (req.method === "GET" || req.method === "HEAD") &&
34
+ prefersMarkdown(req.headers.accept)
35
+ ) {
36
+ const variant = markdownVariantUrl(req.url, routes);
37
+ if (variant) {
38
+ res.setHeader("Vary", "Accept");
39
+ req.url = variant;
40
+ }
41
+ }
42
+ next();
43
+ };
44
+
45
+ /**
46
+ * Blume's Astro integration. Mounts user-authored pages from `pages/` into the
47
+ * generated runtime via `injectRoute`, keeping each file in its original
48
+ * location so relative imports and `getStaticPaths` keep working, and teaches
49
+ * the dev server to honour `Accept: text/markdown`.
50
+ */
51
+ export const blumeIntegration = (
52
+ options: BlumeIntegrationOptions
53
+ ): AstroIntegration => ({
54
+ hooks: {
55
+ "astro:config:setup": ({ injectRoute }) => {
56
+ for (const page of options.pages) {
57
+ injectRoute({
58
+ entrypoint: page.entrypoint,
59
+ pattern: page.pattern,
60
+ prerender: true,
61
+ });
62
+ }
63
+ },
64
+ "astro:server:setup": ({ server }) => {
65
+ // Prepend so the rewrite happens before Astro's own request handler,
66
+ // letting the rewritten URL resolve to the `.md` endpoint.
67
+ server.middlewares.stack.unshift({
68
+ handle: negotiateMarkdown(new Set(options.contentRoutes)),
69
+ route: "",
70
+ });
71
+ },
72
+ },
73
+ name: "blume",
74
+ });
@@ -0,0 +1,131 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import { basename, join } from "pathe";
4
+ import { glob } from "tinyglobby";
5
+
6
+ /** Astro hydration directives Blume applies to a convention island. */
7
+ export type IslandClientMode = "idle" | "load" | "only" | "visible";
8
+
9
+ /** Client framework an island is authored in, inferred from its extension. */
10
+ export type IslandFramework = "react" | "svelte" | "vue";
11
+
12
+ /** A discovered `islands/` component, ready to wrap and expose to MDX. */
13
+ export interface IslandSpec {
14
+ /** Hydration directive, from `export const client` or the default. */
15
+ client: IslandClientMode;
16
+ /** Absolute path to the island source file. */
17
+ file: string;
18
+ /** Astro client framework, used to enable the renderer and for `client:only`. */
19
+ framework: IslandFramework;
20
+ /** Component name used in MDX — the file's basename without extension. */
21
+ name: string;
22
+ }
23
+
24
+ export interface IslandDiscovery {
25
+ islands: IslandSpec[];
26
+ warnings: string[];
27
+ }
28
+
29
+ /** Hydration mode used when an island doesn't declare one. */
30
+ const DEFAULT_CLIENT: IslandClientMode = "visible";
31
+
32
+ const VALID_MODES = new Set<IslandClientMode>([
33
+ "idle",
34
+ "load",
35
+ "only",
36
+ "visible",
37
+ ]);
38
+
39
+ /** Island extensions mapped to the Astro renderer that handles them. */
40
+ const FRAMEWORK_BY_EXT: Record<string, IslandFramework> = {
41
+ jsx: "react",
42
+ svelte: "svelte",
43
+ tsx: "react",
44
+ vue: "vue",
45
+ };
46
+
47
+ // Captures the extension so we can both strip it from the name and pick the
48
+ // framework. Kept in sync with the glob below.
49
+ const ISLAND_FILE = /\.(?<ext>jsx|svelte|tsx|vue)$/u;
50
+
51
+ // Matches `export const client = "load"` (with optional type annotation and
52
+ // either quote style). Read statically so we never execute island code in Node.
53
+ const CLIENT_EXPORT =
54
+ /export\s+const\s+client\s*(?::[^=]+)?=\s*["'](?<mode>\w+)["']/u;
55
+
56
+ export const readClientMode = (
57
+ source: string,
58
+ file: string,
59
+ warnings: string[]
60
+ ): IslandClientMode => {
61
+ const mode = source.match(CLIENT_EXPORT)?.groups?.mode;
62
+ if (!mode) {
63
+ return DEFAULT_CLIENT;
64
+ }
65
+ if (!VALID_MODES.has(mode as IslandClientMode)) {
66
+ warnings.push(
67
+ `Island "${file}" declares an unknown client mode "${mode}"; defaulting to "${DEFAULT_CLIENT}". Use "load", "idle", "visible", or "only".`
68
+ );
69
+ return DEFAULT_CLIENT;
70
+ }
71
+ return mode as IslandClientMode;
72
+ };
73
+
74
+ /**
75
+ * Discover convention islands under `<root>/islands`. Every `.tsx`/`.jsx`/
76
+ * `.vue`/`.svelte` file becomes a globally-available, hydrated MDX component
77
+ * named after the file (which must be PascalCase to be usable as a JSX tag).
78
+ * Hydration defaults to `client:visible`; a file opts out with `export const
79
+ * client = "load" | "idle" | "only"`. Discovery is path-based (a glob), so no
80
+ * user code is executed.
81
+ */
82
+ export const discoverIslands = async (
83
+ root: string
84
+ ): Promise<IslandDiscovery> => {
85
+ const dir = join(root, "islands");
86
+ const matches = await glob(["**/*.{jsx,svelte,tsx,vue}"], {
87
+ absolute: true,
88
+ cwd: dir,
89
+ onlyFiles: true,
90
+ });
91
+ const files = matches.toSorted();
92
+ const sources = await Promise.all(
93
+ files.map((file) => readFile(file, "utf-8"))
94
+ );
95
+
96
+ const islands: IslandSpec[] = [];
97
+ const warnings: string[] = [];
98
+ const seen = new Map<string, string>();
99
+
100
+ for (const [index, file] of files.entries()) {
101
+ const base = basename(file);
102
+ const ext = base.match(ISLAND_FILE)?.groups?.ext;
103
+ const framework = ext ? FRAMEWORK_BY_EXT[ext] : undefined;
104
+ if (!framework) {
105
+ continue;
106
+ }
107
+ const name = base.replace(ISLAND_FILE, "");
108
+ if (!/^[A-Z]/u.test(name)) {
109
+ warnings.push(
110
+ `Island "${file}" must have a PascalCase filename to be used in MDX (e.g. Counter.tsx → <Counter />); skipping it.`
111
+ );
112
+ continue;
113
+ }
114
+ const existing = seen.get(name);
115
+ if (existing) {
116
+ warnings.push(
117
+ `Two islands both resolve to <${name}> ("${existing}" and "${file}"); ignoring the second. Give them distinct filenames.`
118
+ );
119
+ continue;
120
+ }
121
+ seen.set(name, file);
122
+ islands.push({
123
+ client: readClientMode(sources[index] ?? "", file, warnings),
124
+ file,
125
+ framework,
126
+ name,
127
+ });
128
+ }
129
+
130
+ return { islands, warnings };
131
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * HTTP content negotiation for the raw-Markdown variants. The `<route>.md`
3
+ * endpoints already serve a page's source verbatim; these helpers let the dev
4
+ * server honour `Accept: text/markdown` by transparently rewriting a page
5
+ * request to its `.md` variant.
6
+ */
7
+
8
+ interface AcceptEntry {
9
+ q: number;
10
+ type: string;
11
+ }
12
+
13
+ const parseAccept = (accept: string): AcceptEntry[] =>
14
+ accept.split(",").map((part) => {
15
+ const segments = part.trim().split(";");
16
+ const type = (segments[0] ?? "").trim().toLowerCase();
17
+ const qSegment = segments
18
+ .slice(1)
19
+ .map((segment) => segment.trim())
20
+ .find((segment) => segment.startsWith("q="));
21
+ const q = qSegment ? Number.parseFloat(qSegment.slice(2)) : 1;
22
+ return { q: Number.isNaN(q) ? 1 : q, type };
23
+ });
24
+
25
+ /**
26
+ * Whether the client explicitly prefers Markdown over HTML. Browsers never send
27
+ * `text/markdown`, so an ordinary page request (`text/html`, `*​/*`) is false.
28
+ */
29
+ export const prefersMarkdown = (accept: string | null | undefined): boolean => {
30
+ if (!accept) {
31
+ return false;
32
+ }
33
+ let markdownQ = -1;
34
+ let htmlQ = 0;
35
+ for (const { q, type } of parseAccept(accept)) {
36
+ if (type === "text/markdown" || type === "text/x-markdown") {
37
+ markdownQ = Math.max(markdownQ, q);
38
+ } else if (type === "text/html") {
39
+ htmlQ = Math.max(htmlQ, q);
40
+ }
41
+ }
42
+ return markdownQ > 0 && markdownQ >= htmlQ;
43
+ };
44
+
45
+ /**
46
+ * Map a page request URL to its `.md` variant, or `null` when the requested
47
+ * path is not a known content route (Vite/Astro internals, assets, API routes,
48
+ * landing pages, and user `.astro` pages all fall through). `routes` is the set
49
+ * of page paths that have a raw-Markdown variant.
50
+ */
51
+ export const markdownVariantUrl = (
52
+ rawUrl: string | null | undefined,
53
+ routes: ReadonlySet<string>
54
+ ): string | null => {
55
+ if (!rawUrl) {
56
+ return null;
57
+ }
58
+ const queryIndex = rawUrl.indexOf("?");
59
+ const query = queryIndex === -1 ? "" : rawUrl.slice(queryIndex);
60
+ const rawPath = queryIndex === -1 ? rawUrl : rawUrl.slice(0, queryIndex);
61
+ const pathname =
62
+ rawPath !== "/" && rawPath.endsWith("/") ? rawPath.slice(0, -1) : rawPath;
63
+ if (!routes.has(pathname)) {
64
+ return null;
65
+ }
66
+ const target = pathname === "/" ? "/index" : pathname;
67
+ return `${target}.md${query}`;
68
+ };
@@ -0,0 +1,87 @@
1
+ import { extname, relative } from "pathe";
2
+ import { glob } from "tinyglobby";
3
+
4
+ import type { BlumePageRoute } from "./integration.ts";
5
+
6
+ /**
7
+ * Discover user `.astro` pages and map them to route patterns. Files keep their
8
+ * original location; only the route pattern is derived (index -> parent,
9
+ * dynamic `[param]` segments preserved).
10
+ */
11
+ export const discoverPages = async (
12
+ pagesRoot: string
13
+ ): Promise<BlumePageRoute[]> => {
14
+ const files = await glob(["**/*.astro"], {
15
+ absolute: true,
16
+ cwd: pagesRoot,
17
+ onlyFiles: true,
18
+ });
19
+ files.sort();
20
+
21
+ return files.map((file) => {
22
+ const rel = relative(pagesRoot, file);
23
+ const withoutExt = rel.slice(0, rel.length - extname(rel).length);
24
+ const parts = withoutExt.split("/").filter((part) => part !== "index");
25
+ const pattern = parts.length === 0 ? "/" : `/${parts.join("/")}`;
26
+ return { entrypoint: file, pattern };
27
+ });
28
+ };
29
+
30
+ /** A custom-page route that should get a generated OG card. */
31
+ export interface OgCustomRoute {
32
+ /** `og/<slug>.png` path segment; `index` for the site root. */
33
+ slug: string;
34
+ /** Card title. */
35
+ title: string;
36
+ /** Small eyebrow line above the title. */
37
+ eyebrow?: string;
38
+ }
39
+
40
+ /** Skip private (`_partial`, `.well-known`) and Astro dynamic (`[param]`) parts. */
41
+ const PRIVATE_SEGMENT = /^[._]/u;
42
+
43
+ const humanizeSegment = (segment: string): string =>
44
+ segment
45
+ .split(/[-_]/u)
46
+ .filter(Boolean)
47
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
48
+ .join(" ");
49
+
50
+ /**
51
+ * Pick the custom-page routes that should get a generated Open Graph card, with
52
+ * the card's slug and text. OG is otherwise content-route only, so a custom page
53
+ * — most importantly the landing `/`, the most-shared URL — would have no card.
54
+ *
55
+ * Dynamic (`[param]`) routes and private segments (`_partials`, `.well-known`)
56
+ * are skipped: they aren't shareable pages. The home is titled with the site
57
+ * title (eyebrow: the site description); a deeper page is titled from its last
58
+ * path segment.
59
+ */
60
+ export const customOgRoutes = (
61
+ pages: BlumePageRoute[],
62
+ siteTitle: string,
63
+ siteDescription?: string
64
+ ): OgCustomRoute[] => {
65
+ const seen = new Set<string>();
66
+ const routes: OgCustomRoute[] = [];
67
+ for (const { pattern } of pages) {
68
+ const segments = pattern.split("/").filter(Boolean);
69
+ if (
70
+ segments.some((part) => PRIVATE_SEGMENT.test(part) || part.includes("["))
71
+ ) {
72
+ continue;
73
+ }
74
+ const slug = segments.length === 0 ? "index" : segments.join("/");
75
+ if (seen.has(slug)) {
76
+ continue;
77
+ }
78
+ seen.add(slug);
79
+ const last = segments.at(-1);
80
+ routes.push(
81
+ last
82
+ ? { eyebrow: siteTitle, slug, title: humanizeSegment(last) }
83
+ : { eyebrow: siteDescription, slug, title: siteTitle }
84
+ );
85
+ }
86
+ return routes;
87
+ };