blume 1.6.6 → 1.7.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 (99) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/cli/{chunk-etsqspj6.js → chunk-12dzsn9b.js} +140 -23
  3. package/dist/cli/{chunk-etsqspj6.js.map → chunk-12dzsn9b.js.map} +6 -5
  4. package/dist/cli/{chunk-aerwpe14.js → chunk-4ae4f395.js} +164 -51
  5. package/dist/cli/chunk-4ae4f395.js.map +15 -0
  6. package/dist/cli/{chunk-m3p3wahd.js → chunk-52cwcqvp.js} +4 -4
  7. package/dist/cli/{chunk-j00ezcg5.js → chunk-5gfw0q4j.js} +9 -9
  8. package/dist/cli/{chunk-bawgnt8x.js → chunk-6mq7qkve.js} +3 -3
  9. package/dist/cli/{chunk-nyqzjdhj.js → chunk-82atea4k.js} +5 -5
  10. package/dist/cli/{chunk-tc89yh2r.js → chunk-8p3xe5jv.js} +2 -2
  11. package/dist/cli/{chunk-s4k1pnvf.js → chunk-90pdhkpm.js} +11 -11
  12. package/dist/cli/{chunk-n0y172hf.js → chunk-aqjvpd03.js} +4 -4
  13. package/dist/cli/{chunk-x1vrdjyk.js → chunk-h9ekmtz7.js} +5 -5
  14. package/dist/cli/{chunk-f75cqye8.js → chunk-he2zfgah.js} +10 -10
  15. package/dist/cli/{chunk-s4jn7f1q.js → chunk-j5f2wrj5.js} +2 -2
  16. package/dist/cli/{chunk-wkq5tbtq.js → chunk-k0v1f8bb.js} +3 -3
  17. package/dist/cli/{chunk-n4qjabmt.js → chunk-ka5k7cz9.js} +6 -19
  18. package/dist/cli/{chunk-n4qjabmt.js.map → chunk-ka5k7cz9.js.map} +3 -4
  19. package/dist/cli/{chunk-5yvt556e.js → chunk-kmx2mydj.js} +2 -2
  20. package/dist/cli/{chunk-ag1zyr5x.js → chunk-mfm4sjwx.js} +11 -11
  21. package/dist/cli/{chunk-0ewz4trd.js → chunk-np8dmfb0.js} +6 -6
  22. package/dist/cli/{chunk-cnvm6k3e.js → chunk-pdwg3q9g.js} +11 -11
  23. package/dist/cli/{chunk-vv237fp3.js → chunk-q56730e0.js} +26 -12
  24. package/dist/cli/{chunk-vv237fp3.js.map → chunk-q56730e0.js.map} +3 -3
  25. package/dist/cli/{chunk-3k0kzs6d.js → chunk-qvvpnwaz.js} +2 -2
  26. package/dist/cli/{chunk-vv3f8mb6.js → chunk-r99hynxh.js} +25 -24
  27. package/dist/cli/{chunk-vv3f8mb6.js.map → chunk-r99hynxh.js.map} +4 -4
  28. package/dist/cli/{chunk-wb067mv3.js → chunk-vyqj481z.js} +19 -7
  29. package/dist/cli/chunk-vyqj481z.js.map +13 -0
  30. package/dist/cli/{chunk-62qsssnh.js → chunk-x1wvw7a8.js} +411 -145
  31. package/dist/cli/chunk-x1wvw7a8.js.map +40 -0
  32. package/dist/cli/{chunk-9sh49q0h.js → chunk-ywn7t0pb.js} +2 -2
  33. package/dist/cli/index.js +13 -13
  34. package/dist/types/components/layout/nav-utils.d.ts +46 -1
  35. package/dist/types/core/config-input.d.ts +7 -0
  36. package/dist/types/core/schema.d.ts +2 -0
  37. package/dist/types/theme/fonts.d.ts +22 -22
  38. package/docs/02-deployment.mdx +21 -0
  39. package/docs/08-faq.mdx +21 -0
  40. package/docs/configuration/ask-ai.mdx +16 -0
  41. package/docs/content/navigation.mdx +2 -0
  42. package/docs/content/sources.mdx +2 -2
  43. package/docs/content/syntax.mdx +1 -1
  44. package/docs/discoverability/open-graph.mdx +4 -0
  45. package/package.json +1 -1
  46. package/src/ai/ask.ts +31 -4
  47. package/src/astro/generate.ts +142 -5
  48. package/src/astro/integration.ts +12 -1
  49. package/src/astro/module-types.ts +9 -0
  50. package/src/astro/templates.ts +260 -56
  51. package/src/cli/commands/build.ts +28 -0
  52. package/src/components/Icon.astro +24 -0
  53. package/src/components/content/YouTube.astro +1 -1
  54. package/src/components/icon-sprite-middleware.ts +41 -0
  55. package/src/components/icon-sprite.ts +93 -0
  56. package/src/components/layout/Header.astro +1 -1
  57. package/src/components/layout/IconSprite.astro +11 -0
  58. package/src/components/layout/NavTree.astro +156 -188
  59. package/src/components/layout/NavTreeCache.astro +45 -0
  60. package/src/components/layout/NavTreeScript.astro +256 -0
  61. package/src/components/layout/PageActions.astro +11 -5
  62. package/src/components/layout/PageLayout.astro +7 -0
  63. package/src/components/layout/ReferenceLayout.astro +7 -0
  64. package/src/components/layout/RootLayout.astro +30 -2
  65. package/src/components/layout/Search.astro +11 -0
  66. package/src/components/layout/nav-cache.ts +49 -0
  67. package/src/components/layout/nav-utils.ts +87 -1
  68. package/src/core/config-input.ts +7 -0
  69. package/src/core/schema.ts +5 -0
  70. package/src/core/sources/assets.ts +162 -26
  71. package/src/core/sources/notion.ts +60 -1
  72. package/src/markdown/language-icon.ts +64 -20
  73. package/src/markdown/mermaid.ts +11 -0
  74. package/src/og/cache.ts +236 -0
  75. package/src/og/card.ts +12 -4
  76. package/src/og/index.ts +8 -1
  77. package/src/registry/eject.ts +24 -8
  78. package/src/theme/entry.ts +50 -7
  79. package/src/theme/fonts.ts +30 -23
  80. package/dist/cli/chunk-62qsssnh.js.map +0 -36
  81. package/dist/cli/chunk-aerwpe14.js.map +0 -15
  82. package/dist/cli/chunk-wb067mv3.js.map +0 -13
  83. /package/dist/cli/{chunk-m3p3wahd.js.map → chunk-52cwcqvp.js.map} +0 -0
  84. /package/dist/cli/{chunk-j00ezcg5.js.map → chunk-5gfw0q4j.js.map} +0 -0
  85. /package/dist/cli/{chunk-bawgnt8x.js.map → chunk-6mq7qkve.js.map} +0 -0
  86. /package/dist/cli/{chunk-nyqzjdhj.js.map → chunk-82atea4k.js.map} +0 -0
  87. /package/dist/cli/{chunk-tc89yh2r.js.map → chunk-8p3xe5jv.js.map} +0 -0
  88. /package/dist/cli/{chunk-s4k1pnvf.js.map → chunk-90pdhkpm.js.map} +0 -0
  89. /package/dist/cli/{chunk-n0y172hf.js.map → chunk-aqjvpd03.js.map} +0 -0
  90. /package/dist/cli/{chunk-x1vrdjyk.js.map → chunk-h9ekmtz7.js.map} +0 -0
  91. /package/dist/cli/{chunk-f75cqye8.js.map → chunk-he2zfgah.js.map} +0 -0
  92. /package/dist/cli/{chunk-s4jn7f1q.js.map → chunk-j5f2wrj5.js.map} +0 -0
  93. /package/dist/cli/{chunk-wkq5tbtq.js.map → chunk-k0v1f8bb.js.map} +0 -0
  94. /package/dist/cli/{chunk-5yvt556e.js.map → chunk-kmx2mydj.js.map} +0 -0
  95. /package/dist/cli/{chunk-ag1zyr5x.js.map → chunk-mfm4sjwx.js.map} +0 -0
  96. /package/dist/cli/{chunk-0ewz4trd.js.map → chunk-np8dmfb0.js.map} +0 -0
  97. /package/dist/cli/{chunk-cnvm6k3e.js.map → chunk-pdwg3q9g.js.map} +0 -0
  98. /package/dist/cli/{chunk-3k0kzs6d.js.map → chunk-qvvpnwaz.js.map} +0 -0
  99. /package/dist/cli/{chunk-9sh49q0h.js.map → chunk-ywn7t0pb.js.map} +0 -0
@@ -30,9 +30,14 @@ import { buildAskData } from "../ai/ask-data.ts";
30
30
  import { askBackendRuntimeDep, resolveAskBackend } from "../ai/ask.ts";
31
31
  import { buildHomeLinkHeader } from "../ai/link-headers.ts";
32
32
  import { buildRawMarkdown, markdownRoutePaths } from "../ai/markdown.ts";
33
+ import type { RawMarkdownEntry } from "../ai/markdown.ts";
33
34
  import { buildMcpData } from "../ai/mcp/data.ts";
34
35
  import type { McpData } from "../ai/mcp/data.ts";
35
36
  import { buildMcpDiscovery, buildMcpServerCard } from "../ai/mcp/discovery.ts";
37
+ import {
38
+ hasDeferrableGroups,
39
+ navVariants,
40
+ } from "../components/layout/nav-utils.ts";
36
41
  import { normalizeBasePath } from "../core/base-path.ts";
37
42
  import { validateUsedComponents } from "../core/component-diagnostics.ts";
38
43
  import { analyzeComponentOverrides } from "../core/component-overrides.ts";
@@ -68,7 +73,14 @@ import { svgDimensions } from "../core/svg-dimensions.ts";
68
73
  import { trimChar } from "../core/trim.ts";
69
74
  import { resolveTsconfigAliases } from "../core/tsconfig-aliases.ts";
70
75
  import type { Diagnostic, Navigation } from "../core/types.ts";
76
+ import { getBlumeVersion } from "../core/version.ts";
71
77
  import { buildRssFeeds, renderRssFeed } from "../deploy/rss.ts";
78
+ import {
79
+ languageIconCss,
80
+ languageIconSlugsIn,
81
+ } from "../markdown/language-icon.ts";
82
+ import { hasMermaidFence } from "../markdown/mermaid.ts";
83
+ import { ogCacheDir } from "../og/cache.ts";
72
84
  import { missingFontFiles, resolveOgFonts } from "../og/derive.ts";
73
85
  import type { DerivedOgFonts } from "../og/derive.ts";
74
86
  import { resolveOgLogo } from "../og/logo.ts";
@@ -148,10 +160,13 @@ import {
148
160
  runtimeDependencies,
149
161
  runtimePackageTemplate,
150
162
  runtimeTsconfigTemplate,
163
+ featuresTemplate,
164
+ navFragmentTemplate,
151
165
  searchClientTemplate,
152
166
  searchEndpointTemplate,
153
167
  stagedContentDir,
154
168
  } from "./templates.ts";
169
+ import type { ClientFeatures } from "./templates.ts";
155
170
 
156
171
  /** Absolute path to the Blume package `src` directory. */
157
172
  const BLUME_SRC = join(packageRoot(), "src");
@@ -322,10 +337,35 @@ export const blumeDepsDir = (pkgDir: string = packageRoot()): string | null => {
322
337
  };
323
338
 
324
339
  /**
325
- * Point `link` at Blume's dependency directory via a `node_modules` junction,
326
- * replacing a stale junction we own and leaving a real directory untouched.
340
+ * Create `link` as a directory symlink to `target`, falling back to a junction.
327
341
  *
328
- * `lstat`, not `existsSync`, so a broken junction (target since moved) is still
342
+ * The type only matters on Windows, and there a junction is the wrong first
343
+ * choice: Windows can't follow a *relative* symlink reached through a
344
+ * junction (it resolves the relative target against the junction-side path,
345
+ * so it lands outside the store and every lookup is ENOENT), and Bun's
346
+ * isolated linker writes exactly those into Blume's dependency directory
347
+ * whenever it holds symlink privilege. A directory symlink traverses them
348
+ * fine. Without that privilege (no Developer Mode, non-admin shell) creating
349
+ * one fails, so fall back to a junction — and in that session the installer
350
+ * had no privilege either, so the deps are absolute junctions a junction can
351
+ * follow. Exported for testing.
352
+ */
353
+ export const symlinkDir = async (
354
+ target: string,
355
+ link: string
356
+ ): Promise<void> => {
357
+ try {
358
+ await symlink(target, link, "dir");
359
+ } catch {
360
+ await symlink(target, link, "junction");
361
+ }
362
+ };
363
+
364
+ /**
365
+ * Point `link` at Blume's dependency directory via a `node_modules` link,
366
+ * replacing a stale link we own and leaving a real directory untouched.
367
+ *
368
+ * `lstat`, not `existsSync`, so a broken link (target since moved) is still
329
369
  * detected — `existsSync` follows the link and reports a dangling one as absent.
330
370
  */
331
371
  const linkDepsJunction = async (
@@ -356,7 +396,7 @@ const linkDepsJunction = async (
356
396
  await rm(link, { force: true });
357
397
  }
358
398
  await mkdir(dirname(link), { recursive: true });
359
- await symlink(depsDir, link, "junction");
399
+ await symlinkDir(depsDir, link);
360
400
  };
361
401
 
362
402
  /** Read the `version` field of a `package.json`, or null when unreadable. */
@@ -1894,6 +1934,84 @@ const assertFontFilesExist = (project: BlumeProject): void => {
1894
1934
  }
1895
1935
  };
1896
1936
 
1937
+ /**
1938
+ * The client libraries a site needs (see `featuresTemplate`): the EPUB
1939
+ * generator when `export.epub` is on, and the Mermaid element when any page's
1940
+ * source has a mermaid fence — read from the raw-Markdown mirrors (every
1941
+ * route's verbatim source) and, for sources that carry their text on the
1942
+ * page record, the record itself.
1943
+ */
1944
+ export const clientFeaturesFrom = (
1945
+ project: BlumeProject,
1946
+ rawMarkdown: Record<string, RawMarkdownEntry>
1947
+ ): ClientFeatures => ({
1948
+ epub: project.config.export.epub,
1949
+ mermaid:
1950
+ Object.values(rawMarkdown).some((entry) =>
1951
+ hasMermaidFence(entry.mdx ?? entry.md ?? "")
1952
+ ) ||
1953
+ project.graph.pages.some(
1954
+ (page) => page.body !== undefined && hasMermaidFence(page.body.text)
1955
+ ),
1956
+ });
1957
+
1958
+ /**
1959
+ * The theme's code-block icon rules for the languages the site's Markdown
1960
+ * uses (see `languageIconCss`), read from the raw-Markdown mirrors and the
1961
+ * page records that carry their text.
1962
+ */
1963
+ export const languageIconCssFrom = (
1964
+ project: BlumeProject,
1965
+ rawMarkdown: Record<string, RawMarkdownEntry>
1966
+ ): string =>
1967
+ languageIconCss(
1968
+ languageIconSlugsIn(
1969
+ [
1970
+ ...Object.values(rawMarkdown).map(
1971
+ (entry) => entry.mdx ?? entry.md ?? ""
1972
+ ),
1973
+ ...project.graph.pages.map((page) => page.body?.text ?? ""),
1974
+ ].join("\n")
1975
+ )
1976
+ );
1977
+
1978
+ /** {@link languageIconCssFrom} over a fresh read of the raw Markdown (eject). */
1979
+ export const languageIconCssFor = async (
1980
+ project: BlumeProject
1981
+ ): Promise<string> =>
1982
+ languageIconCssFrom(project, await buildRawMarkdown(project));
1983
+
1984
+ /**
1985
+ * The deferred sidebar fragments page, only when some group is a disclosure
1986
+ * or a drill-in panel — a flat sidebar renders every row on every page, so
1987
+ * there is nothing to fetch. A previous pass's page is an orphan the
1988
+ * generator removes when the sidebar goes flat again.
1989
+ */
1990
+ const writeNavFragments = (
1991
+ write: (path: string, content: string) => Promise<boolean>,
1992
+ srcDir: string,
1993
+ navFragments: boolean
1994
+ ): Promise<boolean> =>
1995
+ navFragments
1996
+ ? write(
1997
+ join(
1998
+ srcDir,
1999
+ "pages",
2000
+ "blume-nav",
2001
+ "[version]",
2002
+ "[locale]",
2003
+ "[id].astro"
2004
+ ),
2005
+ navFragmentTemplate()
2006
+ )
2007
+ : Promise.resolve(false);
2008
+
2009
+ /** {@link clientFeaturesFrom} over a fresh read of the raw Markdown (eject). */
2010
+ export const clientFeaturesFor = async (
2011
+ project: BlumeProject
2012
+ ): Promise<ClientFeatures> =>
2013
+ clientFeaturesFrom(project, await buildRawMarkdown(project));
2014
+
1897
2015
  /**
1898
2016
  * Write (or update) the generated `.blume/` Astro runtime for a project.
1899
2017
  * Only files whose content changed are rewritten so Vite HMR stays fast.
@@ -1909,6 +2027,7 @@ export const generateRuntime = async (
1909
2027
  const askPath = join(srcDir, "generated", "Ask.astro");
1910
2028
  const themePath = join(srcDir, "generated", "app.css");
1911
2029
  const searchClientPath = join(srcDir, "generated", "search-client.ts");
2030
+ const featuresPath = join(srcDir, "generated", "features.ts");
1912
2031
  const examplesPath = join(srcDir, "generated", "examples.ts");
1913
2032
  const examplesThemePath = join(srcDir, "generated", "examples.css");
1914
2033
 
@@ -1937,6 +2056,14 @@ export const generateRuntime = async (
1937
2056
  const askEnabled = config.ai.ask?.enabled ?? false;
1938
2057
  const exportPdf = config.export.pdf;
1939
2058
  const exportEpub = config.export.epub;
2059
+ // Every route's source Markdown: published as `blume:raw-markdown` below,
2060
+ // and inspected here for the client features the site needs.
2061
+ const rawMarkdown = await buildRawMarkdown(project);
2062
+ const clientFeatures = clientFeaturesFrom(project, rawMarkdown);
2063
+ const navFragments = navVariants(project.graph).some(({ navigation }) =>
2064
+ hasDeferrableGroups(navigation.sidebar)
2065
+ );
2066
+ const languageIcons = languageIconCssFrom(project, rawMarkdown);
1940
2067
  // Staged (non-filesystem) sources materialize into `.blume/content`; keyed by
1941
2068
  // entryId so i18n duplicates of one entry write a single file. Collected here
1942
2069
  // so math detection also sees staged bodies (they never live under root).
@@ -2060,6 +2187,8 @@ export const generateRuntime = async (
2060
2187
  context,
2061
2188
  examplesPath,
2062
2189
  examplesThemePath,
2190
+ features: clientFeatures,
2191
+ featuresPath,
2063
2192
  integrationBridge,
2064
2193
  needsReact,
2065
2194
  needsSvelte,
@@ -2093,9 +2222,11 @@ export const generateRuntime = async (
2093
2222
  exportEpub,
2094
2223
  exportPdf,
2095
2224
  mathEnabled: usesMath,
2225
+ navFragments,
2096
2226
  needsReact,
2097
2227
  })
2098
2228
  ),
2229
+ writeNavFragments(write, srcDir, navFragments),
2099
2230
  // The header's Ask trigger, behind the `blume:ask` alias. Always written
2100
2231
  // (even when Ask is off, as a component that renders nothing) so the alias
2101
2232
  // resolves — the same contract as `blume:search-client`.
@@ -2127,6 +2258,7 @@ export const generateRuntime = async (
2127
2258
  themePath,
2128
2259
  tailwindEntryTemplate({
2129
2260
  configTokens: `${buildThemeCss(config.theme)}${buildFontsCss(config.theme.fonts)}`,
2261
+ languageIcons,
2130
2262
  sources: [
2131
2263
  `${BLUME_SRC}/**/*.{astro,ts,tsx}`,
2132
2264
  `${context.root}/**/*.{astro,mdx,ts,tsx}`,
@@ -2189,6 +2321,10 @@ export const generateRuntime = async (
2189
2321
  ogRoutes,
2190
2322
  {
2191
2323
  ...projectOgFonts(project),
2324
+ cache: {
2325
+ dir: ogCacheDir(project.context),
2326
+ version: getBlumeVersion(),
2327
+ },
2192
2328
  pageDescriptions: config.seo.og.description !== false,
2193
2329
  },
2194
2330
  changelogIndex
@@ -2203,6 +2339,7 @@ export const generateRuntime = async (
2203
2339
  changelogIndexTemplate({
2204
2340
  exportEpub,
2205
2341
  exportPdf,
2342
+ mathEnabled: usesMath,
2206
2343
  needsReact,
2207
2344
  staged: hasStaged,
2208
2345
  })
@@ -2225,6 +2362,7 @@ export const generateRuntime = async (
2225
2362
  }),
2226
2363
  writeNotFoundPage(write, srcDir, pages, project.graph.pages),
2227
2364
  write(searchClientPath, searchClientTemplate(config)),
2365
+ write(featuresPath, featuresTemplate(clientFeatures)),
2228
2366
  ]);
2229
2367
 
2230
2368
  // Client-loaded providers (orama, flexsearch) ship a static index + endpoint.
@@ -2253,7 +2391,6 @@ export const generateRuntime = async (
2253
2391
  `${JSON.stringify(buildIncludeGraph(project.graph.pages))}\n`
2254
2392
  );
2255
2393
 
2256
- const rawMarkdown = await buildRawMarkdown(project);
2257
2394
  modules.set("blume:raw-markdown", JSON.stringify(rawMarkdown));
2258
2395
  // The originals behind the rewritten `/blume-assets/content/…` references in
2259
2396
  // the agent-facing Markdown, plus the endpoint that serves them (and the
@@ -374,8 +374,19 @@ export const blumeIntegration = (
374
374
  filename: MODULE_TYPES_FILE,
375
375
  });
376
376
  },
377
- "astro:config:setup": ({ createCodegenDir, injectRoute }) => {
377
+ "astro:config:setup": ({
378
+ addMiddleware,
379
+ createCodegenDir,
380
+ injectRoute,
381
+ }) => {
378
382
  codegenDir = createCodegenDir();
383
+ // Splices each page's icon sprite in once the page has rendered (see
384
+ // components/icon-sprite-middleware.ts). Innermost, so a project's
385
+ // own middleware sees the finished HTML.
386
+ addMiddleware({
387
+ entrypoint: "blume/components/icon-sprite-middleware.ts",
388
+ order: "post",
389
+ });
379
390
  for (const page of options.pages) {
380
391
  injectRoute({
381
392
  entrypoint: page.entrypoint,
@@ -66,6 +66,15 @@ declare module "blume:openapi" {
66
66
  export default specs;
67
67
  }
68
68
 
69
+ declare module "blume:features" {
70
+ /** Registers the <blume-mermaid> element; null when no page has a mermaid fence. */
71
+ export const loadMermaid: (() => Promise<unknown>) | null;
72
+ /** The EPUB generator's browser bundle; null when export.epub is off. */
73
+ export const loadEpub:
74
+ | (() => Promise<typeof import("epub-gen-memory/bundle")>)
75
+ | null;
76
+ }
77
+
69
78
  declare module "blume:search-client" {
70
79
  export const createSearch: () =>
71
80
  | import("blume/components/layout/search/types.ts").SearchFn