blume 1.6.5 → 1.7.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 (179) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/bin/blume.mjs +3 -2
  3. package/dist/cli/chunk-0qhq7b8q.js +111 -0
  4. package/dist/cli/chunk-0qhq7b8q.js.map +11 -0
  5. package/dist/cli/chunk-18tjv4f7.js +96 -0
  6. package/dist/cli/chunk-18tjv4f7.js.map +10 -0
  7. package/dist/cli/chunk-27gtm2ym.js +69 -0
  8. package/dist/cli/chunk-27gtm2ym.js.map +11 -0
  9. package/dist/cli/chunk-2aj8ddew.js +72 -0
  10. package/dist/cli/chunk-2aj8ddew.js.map +10 -0
  11. package/dist/cli/chunk-3r94j3tc.js +221 -0
  12. package/dist/cli/chunk-3r94j3tc.js.map +10 -0
  13. package/dist/cli/chunk-4trphnvy.js +102 -0
  14. package/dist/cli/chunk-4trphnvy.js.map +11 -0
  15. package/dist/cli/chunk-4xyggvgf.js +21 -0
  16. package/dist/cli/chunk-4xyggvgf.js.map +10 -0
  17. package/dist/cli/chunk-5d4q7121.js +4064 -0
  18. package/dist/cli/chunk-5d4q7121.js.map +40 -0
  19. package/dist/cli/chunk-5hs6gb7n.js +32 -0
  20. package/dist/cli/chunk-5hs6gb7n.js.map +10 -0
  21. package/dist/cli/chunk-6kzzpsx8.js +26 -0
  22. package/dist/cli/chunk-6kzzpsx8.js.map +10 -0
  23. package/dist/cli/chunk-8gnpdsn1.js +952 -0
  24. package/dist/cli/chunk-8gnpdsn1.js.map +12 -0
  25. package/dist/cli/chunk-9qs6acpw.js +176 -0
  26. package/dist/cli/chunk-9qs6acpw.js.map +10 -0
  27. package/dist/cli/chunk-agy5rzxy.js +2453 -0
  28. package/dist/cli/chunk-agy5rzxy.js.map +15 -0
  29. package/dist/cli/chunk-bcy492zc.js +16 -0
  30. package/dist/cli/chunk-bcy492zc.js.map +10 -0
  31. package/dist/cli/chunk-btfr9yvw.js +41 -0
  32. package/dist/cli/chunk-btfr9yvw.js.map +10 -0
  33. package/dist/cli/chunk-cbjnx4s8.js +73 -0
  34. package/dist/cli/chunk-cbjnx4s8.js.map +10 -0
  35. package/dist/cli/chunk-cfw6x4rm.js +1967 -0
  36. package/dist/cli/chunk-cfw6x4rm.js.map +34 -0
  37. package/dist/cli/chunk-ckh3a410.js +277 -0
  38. package/dist/cli/chunk-ckh3a410.js.map +11 -0
  39. package/dist/cli/chunk-drke6t0h.js +259 -0
  40. package/dist/cli/chunk-drke6t0h.js.map +11 -0
  41. package/dist/cli/chunk-ev67ycx0.js +15 -0
  42. package/dist/cli/chunk-ev67ycx0.js.map +10 -0
  43. package/dist/cli/chunk-ey89bjj1.js +209 -0
  44. package/dist/cli/chunk-ey89bjj1.js.map +11 -0
  45. package/dist/cli/chunk-j6pxe0dt.js +69 -0
  46. package/dist/cli/chunk-j6pxe0dt.js.map +11 -0
  47. package/dist/cli/chunk-jk1zwka1.js +387 -0
  48. package/dist/cli/chunk-jk1zwka1.js.map +12 -0
  49. package/dist/cli/chunk-jtb45atp.js +467 -0
  50. package/dist/cli/chunk-jtb45atp.js.map +14 -0
  51. package/dist/cli/chunk-jxkxjsc1.js +76 -0
  52. package/dist/cli/chunk-jxkxjsc1.js.map +10 -0
  53. package/dist/cli/chunk-kwx90v78.js +81 -0
  54. package/dist/cli/chunk-kwx90v78.js.map +10 -0
  55. package/dist/cli/chunk-n0nyat6g.js +30 -0
  56. package/dist/cli/chunk-n0nyat6g.js.map +10 -0
  57. package/dist/cli/chunk-pxj10x8y.js +35 -0
  58. package/dist/cli/chunk-pxj10x8y.js.map +10 -0
  59. package/dist/cli/chunk-qq9nm3qd.js +1141 -0
  60. package/dist/cli/chunk-qq9nm3qd.js.map +19 -0
  61. package/dist/cli/chunk-s102bysw.js +5170 -0
  62. package/dist/cli/chunk-s102bysw.js.map +47 -0
  63. package/dist/cli/chunk-s5dsk8bj.js +769 -0
  64. package/dist/cli/chunk-s5dsk8bj.js.map +13 -0
  65. package/dist/cli/chunk-s5e5jt53.js +227 -0
  66. package/dist/cli/chunk-s5e5jt53.js.map +11 -0
  67. package/dist/cli/chunk-sbdqrjbb.js +81 -0
  68. package/dist/cli/chunk-sbdqrjbb.js.map +10 -0
  69. package/dist/cli/chunk-tnskyrej.js +117 -0
  70. package/dist/cli/chunk-tnskyrej.js.map +10 -0
  71. package/dist/cli/chunk-v2ymm99c.js +1016 -0
  72. package/dist/cli/chunk-v2ymm99c.js.map +13 -0
  73. package/dist/cli/chunk-v5mm027v.js +185 -0
  74. package/dist/cli/chunk-v5mm027v.js.map +11 -0
  75. package/dist/cli/chunk-vt8fgygt.js +23 -0
  76. package/dist/cli/chunk-vt8fgygt.js.map +10 -0
  77. package/dist/cli/chunk-vxv4x1n8.js +17 -0
  78. package/dist/cli/chunk-vxv4x1n8.js.map +10 -0
  79. package/dist/cli/chunk-wd27zjcz.js +60 -0
  80. package/dist/cli/chunk-wd27zjcz.js.map +10 -0
  81. package/dist/cli/chunk-x66c5yjn.js +23 -0
  82. package/dist/cli/chunk-x66c5yjn.js.map +10 -0
  83. package/dist/cli/chunk-xv91q4nm.js +5314 -0
  84. package/dist/cli/chunk-xv91q4nm.js.map +58 -0
  85. package/dist/cli/chunk-y3g15rvv.js +679 -0
  86. package/dist/cli/chunk-y3g15rvv.js.map +15 -0
  87. package/dist/cli/chunk-ye9zdkgv.js +136 -0
  88. package/dist/cli/chunk-ye9zdkgv.js.map +10 -0
  89. package/dist/cli/chunk-ynacq3ev.js +1062 -0
  90. package/dist/cli/chunk-ynacq3ev.js.map +25 -0
  91. package/dist/cli/chunk-zr3ygrq3.js +54 -0
  92. package/dist/cli/chunk-zr3ygrq3.js.map +10 -0
  93. package/dist/cli/index.js +55 -27597
  94. package/dist/cli/index.js.map +5 -243
  95. package/dist/types/ai/ask-context.d.ts +26 -0
  96. package/dist/types/components/layout/nav-utils.d.ts +33 -1
  97. package/dist/types/core/code-fences.d.ts +11 -0
  98. package/dist/types/core/package-root.d.ts +1 -1
  99. package/dist/types/core/schema.d.ts +70 -0
  100. package/dist/types/theme/fonts.d.ts +22 -22
  101. package/docs/02-deployment.mdx +22 -1
  102. package/docs/configuration/ask-ai.mdx +1 -1
  103. package/docs/configuration/customization.mdx +2 -9
  104. package/docs/content/navigation.mdx +2 -0
  105. package/docs/content/syntax.mdx +1 -1
  106. package/docs/discoverability/open-graph.mdx +4 -0
  107. package/docs/reference/cli.mdx +1 -1
  108. package/package.json +4 -2
  109. package/src/ai/api/handlers.ts +4 -7
  110. package/src/ai/api/paths.ts +8 -0
  111. package/src/ai/api/spec.ts +2 -1
  112. package/src/ai/ask-context.ts +378 -22
  113. package/src/astro/generate.ts +161 -28
  114. package/src/astro/include-hmr.ts +10 -13
  115. package/src/astro/include-refresh.ts +0 -0
  116. package/src/astro/index.ts +6 -1
  117. package/src/astro/integration.ts +280 -53
  118. package/src/astro/module-types.ts +83 -0
  119. package/src/astro/templates.ts +256 -108
  120. package/src/audit/image-size.ts +10 -8
  121. package/src/cli/command-meta.ts +77 -0
  122. package/src/cli/commands/add.ts +2 -4
  123. package/src/cli/commands/audit.ts +2 -4
  124. package/src/cli/commands/build.ts +70 -346
  125. package/src/cli/commands/check.ts +2 -4
  126. package/src/cli/commands/dev.ts +31 -42
  127. package/src/cli/commands/doctor.ts +2 -4
  128. package/src/cli/commands/eject.ts +3 -41
  129. package/src/cli/commands/eval.ts +2 -5
  130. package/src/cli/commands/init.ts +2 -4
  131. package/src/cli/commands/mcp-stdio.ts +2 -5
  132. package/src/cli/commands/preview.ts +3 -5
  133. package/src/cli/commands/sync.ts +2 -4
  134. package/src/cli/commands/translate.ts +2 -5
  135. package/src/cli/commands/validate.ts +2 -4
  136. package/src/cli/commands/version.ts +2 -4
  137. package/src/cli/eject-scripts.ts +0 -45
  138. package/src/cli/host-args.ts +16 -0
  139. package/src/cli/index.ts +84 -35
  140. package/src/cli/lazy-command.ts +47 -0
  141. package/src/components/Icon.astro +24 -0
  142. package/src/components/content/GithubInfo.astro +4 -1
  143. package/src/components/icon-sprite-middleware.ts +41 -0
  144. package/src/components/icon-sprite.ts +93 -0
  145. package/src/components/layout/IconSprite.astro +11 -0
  146. package/src/components/layout/NavTree.astro +156 -188
  147. package/src/components/layout/NavTreeCache.astro +45 -0
  148. package/src/components/layout/NavTreeScript.astro +256 -0
  149. package/src/components/layout/PageActions.astro +11 -5
  150. package/src/components/layout/PageLayout.astro +21 -3
  151. package/src/components/layout/ReferenceLayout.astro +21 -4
  152. package/src/components/layout/RootLayout.astro +44 -6
  153. package/src/components/layout/nav-cache.ts +49 -0
  154. package/src/components/layout/nav-utils.ts +69 -1
  155. package/src/components/layout/page-locale.ts +29 -0
  156. package/src/core/api-name.ts +18 -0
  157. package/src/core/code-fences.ts +48 -0
  158. package/src/core/content-assets.ts +3 -7
  159. package/src/core/includes.ts +3 -7
  160. package/src/core/package-root.ts +1 -1
  161. package/src/core/schema.ts +19 -0
  162. package/src/core/sources/normalize.ts +2 -37
  163. package/src/core/sources/obsidian.ts +3 -2
  164. package/src/core/svg-dimensions.ts +97 -0
  165. package/src/core/version-cut.ts +2 -2
  166. package/src/deploy/artifacts.ts +370 -0
  167. package/src/deploy/cloudflare-negotiation.ts +97 -32
  168. package/src/deploy/function-bundle.ts +66 -20
  169. package/src/deploy/sitemap.ts +6 -0
  170. package/src/deploy/vercel-negotiation.ts +8 -30
  171. package/src/markdown/language-icon.ts +64 -20
  172. package/src/markdown/mermaid.ts +11 -0
  173. package/src/og/cache.ts +236 -0
  174. package/src/og/card.ts +18 -16
  175. package/src/og/index.ts +8 -1
  176. package/src/openapi/render-mdx.ts +9 -5
  177. package/src/registry/eject.ts +23 -10
  178. package/src/theme/entry.ts +41 -7
  179. package/src/theme/fonts.ts +30 -23
@@ -12,7 +12,6 @@ import {
12
12
  import { createRequire } from "node:module";
13
13
  import { pathToFileURL } from "node:url";
14
14
 
15
- import { imageSize } from "image-size";
16
15
  import pMap from "p-map";
17
16
  import {
18
17
  basename,
@@ -29,10 +28,16 @@ import { OPENAPI_PATH } from "../ai/api/paths.ts";
29
28
  import { buildApiSpec } from "../ai/api/spec.ts";
30
29
  import { buildAskData } from "../ai/ask-data.ts";
31
30
  import { askBackendRuntimeDep, resolveAskBackend } from "../ai/ask.ts";
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";
@@ -64,10 +69,18 @@ import { packageRoot } from "../core/package-root.ts";
64
69
  import type { BlumeProject } from "../core/project-graph.ts";
65
70
  import type { ResolvedConfig } from "../core/schema.ts";
66
71
  import { resolveDocsCollection } from "../core/sources/resolve.ts";
72
+ import { svgDimensions } from "../core/svg-dimensions.ts";
67
73
  import { trimChar } from "../core/trim.ts";
68
74
  import { resolveTsconfigAliases } from "../core/tsconfig-aliases.ts";
69
75
  import type { Diagnostic, Navigation } from "../core/types.ts";
76
+ import { getBlumeVersion } from "../core/version.ts";
70
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";
71
84
  import { missingFontFiles, resolveOgFonts } from "../og/derive.ts";
72
85
  import type { DerivedOgFonts } from "../og/derive.ts";
73
86
  import { resolveOgLogo } from "../og/logo.ts";
@@ -104,6 +117,7 @@ import {
104
117
  exampleMarkdownLookup,
105
118
  exampleScanRoots,
106
119
  } from "./examples.ts";
120
+ import { publishDevNegotiation } from "./integration.ts";
107
121
  import { discoverIslands } from "./islands.ts";
108
122
  import {
109
123
  customOgRoutes,
@@ -121,7 +135,6 @@ import {
121
135
  changelogIndexTemplate,
122
136
  contentAssetsEndpointTemplate,
123
137
  contentConfigTemplate,
124
- envTemplate,
125
138
  exampleMapTemplate,
126
139
  exampleWrapperTemplate,
127
140
  examplesPageTemplate,
@@ -147,10 +160,13 @@ import {
147
160
  runtimeDependencies,
148
161
  runtimePackageTemplate,
149
162
  runtimeTsconfigTemplate,
163
+ featuresTemplate,
164
+ navFragmentTemplate,
150
165
  searchClientTemplate,
151
166
  searchEndpointTemplate,
152
167
  stagedContentDir,
153
168
  } from "./templates.ts";
169
+ import type { ClientFeatures } from "./templates.ts";
154
170
 
155
171
  /** Absolute path to the Blume package `src` directory. */
156
172
  const BLUME_SRC = join(packageRoot(), "src");
@@ -321,10 +337,35 @@ export const blumeDepsDir = (pkgDir: string = packageRoot()): string | null => {
321
337
  };
322
338
 
323
339
  /**
324
- * Point `link` at Blume's dependency directory via a `node_modules` junction,
325
- * 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.
341
+ *
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.
326
367
  *
327
- * `lstat`, not `existsSync`, so a broken junction (target since moved) is still
368
+ * `lstat`, not `existsSync`, so a broken link (target since moved) is still
328
369
  * detected — `existsSync` follows the link and reports a dangling one as absent.
329
370
  */
330
371
  const linkDepsJunction = async (
@@ -355,7 +396,7 @@ const linkDepsJunction = async (
355
396
  await rm(link, { force: true });
356
397
  }
357
398
  await mkdir(dirname(link), { recursive: true });
358
- await symlink(depsDir, link, "junction");
399
+ await symlinkDir(depsDir, link);
359
400
  };
360
401
 
361
402
  /** Read the `version` field of a `package.json`, or null when unreadable. */
@@ -906,24 +947,13 @@ interface LogoDimensions {
906
947
  }
907
948
 
908
949
  /**
909
- * Read dimensions from an SVG's explicit size or its view box. Measured with
910
- * image-size — the same parser og/card.ts uses for the OG brand mark, so the
911
- * header and the card can't disagree about one logo — which also tolerates
912
- * the spellings the old regex missed (unquoted values, `em`/`pt` lengths, a
913
- * `>` inside another attribute). An SVG with no usable size returns partial
914
- * dimensions or throws; both collapse to undefined.
950
+ * Read dimensions from an SVG's explicit size or its view box, with the same
951
+ * root-tag parser og/card.ts uses for the OG brand mark so the header and the
952
+ * card can't disagree about one logo. An SVG with no usable size collapses to
953
+ * undefined.
915
954
  */
916
- const svgDimensions = (svg: string | undefined): LogoDimensions | undefined => {
917
- if (!svg) {
918
- return;
919
- }
920
- try {
921
- const { height, width } = imageSize(Buffer.from(svg));
922
- return height && width ? { height, width } : undefined;
923
- } catch {
924
- return undefined;
925
- }
926
- };
955
+ const logoDimensions = (svg: string | undefined): LogoDimensions | undefined =>
956
+ svg ? (svgDimensions(svg) ?? undefined) : undefined;
927
957
 
928
958
  /** Read a local SVG logo from the project root or public directory. */
929
959
  const readLogoSvg = (
@@ -988,8 +1018,8 @@ const resolveLogo = (project: BlumeProject): BlumeLogo | null => {
988
1018
  return { alt, href: brandHref, svg: lightSvg, text };
989
1019
  }
990
1020
 
991
- const lightDimensions = svgDimensions(lightSvg);
992
- const darkDimensions = svgDimensions(darkSvg);
1021
+ const lightDimensions = logoDimensions(lightSvg);
1022
+ const darkDimensions = logoDimensions(darkSvg);
993
1023
  const dimensions =
994
1024
  lightDimensions || darkDimensions
995
1025
  ? { dark: darkDimensions, light: lightDimensions }
@@ -1904,6 +1934,84 @@ const assertFontFilesExist = (project: BlumeProject): void => {
1904
1934
  }
1905
1935
  };
1906
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
+
1907
2015
  /**
1908
2016
  * Write (or update) the generated `.blume/` Astro runtime for a project.
1909
2017
  * Only files whose content changed are rewritten so Vite HMR stays fast.
@@ -1919,6 +2027,7 @@ export const generateRuntime = async (
1919
2027
  const askPath = join(srcDir, "generated", "Ask.astro");
1920
2028
  const themePath = join(srcDir, "generated", "app.css");
1921
2029
  const searchClientPath = join(srcDir, "generated", "search-client.ts");
2030
+ const featuresPath = join(srcDir, "generated", "features.ts");
1922
2031
  const examplesPath = join(srcDir, "generated", "examples.ts");
1923
2032
  const examplesThemePath = join(srcDir, "generated", "examples.css");
1924
2033
 
@@ -1937,9 +2046,24 @@ export const generateRuntime = async (
1937
2046
 
1938
2047
  const depsLinkWarning = await ensureDepsLink(out);
1939
2048
 
2049
+ // The dev negotiation inputs. Published in memory (below) rather than baked
2050
+ // into the generated config, so a content-route change never rewrites
2051
+ // `astro.config.mjs` — which would restart the dev server in place.
2052
+ const contentRoutes = markdownRoutePaths(project);
2053
+ const homeLinkHeader =
2054
+ buildHomeLinkHeader(config, contentRoutes) ?? undefined;
2055
+
1940
2056
  const askEnabled = config.ai.ask?.enabled ?? false;
1941
2057
  const exportPdf = config.export.pdf;
1942
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);
1943
2067
  // Staged (non-filesystem) sources materialize into `.blume/content`; keyed by
1944
2068
  // entryId so i18n duplicates of one entry write a single file. Collected here
1945
2069
  // so math detection also sees staged bodies (they never live under root).
@@ -2059,10 +2183,12 @@ export const generateRuntime = async (
2059
2183
  askPath,
2060
2184
  config,
2061
2185
  contentRoot: docsCollection.base,
2062
- contentRoutes: markdownRoutePaths(project),
2186
+ contentRoutes,
2063
2187
  context,
2064
2188
  examplesPath,
2065
2189
  examplesThemePath,
2190
+ features: clientFeatures,
2191
+ featuresPath,
2066
2192
  integrationBridge,
2067
2193
  needsReact,
2068
2194
  needsSvelte,
@@ -2080,7 +2206,6 @@ export const generateRuntime = async (
2080
2206
  )
2081
2207
  ),
2082
2208
  write(join(out, "tsconfig.json"), runtimeTsconfigTemplate()),
2083
- write(join(srcDir, "env.d.ts"), envTemplate()),
2084
2209
  write(
2085
2210
  join(srcDir, "content.config.ts"),
2086
2211
  contentConfigTemplate({
@@ -2097,9 +2222,11 @@ export const generateRuntime = async (
2097
2222
  exportEpub,
2098
2223
  exportPdf,
2099
2224
  mathEnabled: usesMath,
2225
+ navFragments,
2100
2226
  needsReact,
2101
2227
  })
2102
2228
  ),
2229
+ writeNavFragments(write, srcDir, navFragments),
2103
2230
  // The header's Ask trigger, behind the `blume:ask` alias. Always written
2104
2231
  // (even when Ask is off, as a component that renders nothing) so the alias
2105
2232
  // resolves — the same contract as `blume:search-client`.
@@ -2131,6 +2258,7 @@ export const generateRuntime = async (
2131
2258
  themePath,
2132
2259
  tailwindEntryTemplate({
2133
2260
  configTokens: `${buildThemeCss(config.theme)}${buildFontsCss(config.theme.fonts)}`,
2261
+ languageIcons,
2134
2262
  sources: [
2135
2263
  `${BLUME_SRC}/**/*.{astro,ts,tsx}`,
2136
2264
  `${context.root}/**/*.{astro,mdx,ts,tsx}`,
@@ -2193,6 +2321,10 @@ export const generateRuntime = async (
2193
2321
  ogRoutes,
2194
2322
  {
2195
2323
  ...projectOgFonts(project),
2324
+ cache: {
2325
+ dir: ogCacheDir(project.context),
2326
+ version: getBlumeVersion(),
2327
+ },
2196
2328
  pageDescriptions: config.seo.og.description !== false,
2197
2329
  },
2198
2330
  changelogIndex
@@ -2229,6 +2361,7 @@ export const generateRuntime = async (
2229
2361
  }),
2230
2362
  writeNotFoundPage(write, srcDir, pages, project.graph.pages),
2231
2363
  write(searchClientPath, searchClientTemplate(config)),
2364
+ write(featuresPath, featuresTemplate(clientFeatures)),
2232
2365
  ]);
2233
2366
 
2234
2367
  // Client-loaded providers (orama, flexsearch) ship a static index + endpoint.
@@ -2257,7 +2390,6 @@ export const generateRuntime = async (
2257
2390
  `${JSON.stringify(buildIncludeGraph(project.graph.pages))}\n`
2258
2391
  );
2259
2392
 
2260
- const rawMarkdown = await buildRawMarkdown(project);
2261
2393
  modules.set("blume:raw-markdown", JSON.stringify(rawMarkdown));
2262
2394
  // The originals behind the rewritten `/blume-assets/content/…` references in
2263
2395
  // the agent-facing Markdown, plus the endpoint that serves them (and the
@@ -2391,6 +2523,7 @@ export const generateRuntime = async (
2391
2523
  // Publish last, once every page that imports a module is on disk: a live
2392
2524
  // dev server invalidates the changed modules and reloads the browser against
2393
2525
  // the finished tree, never a half-written one.
2526
+ publishDevNegotiation({ contentRoutes, homeLinkHeader });
2394
2527
  publishRuntimeModules(modules);
2395
2528
 
2396
2529
  return { structuralChange: structural.some(Boolean), warnings };
@@ -1,4 +1,6 @@
1
- import { readFile, utimes } from "node:fs/promises";
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import { refreshBlumeContent } from "./integration.ts";
2
4
 
3
5
  /**
4
6
  * Dev-server invalidation for `<include>` partials. A partial is not an Astro
@@ -54,25 +56,20 @@ export const includeHmrPlugin = (graphPath: string): IncludeHmrPlugin => ({
54
56
  return;
55
57
  }
56
58
  const { moduleGraph, ws } = ctx.server;
57
- const now = new Date();
58
59
  for (const includer of includers) {
59
60
  for (const mod of moduleGraph.getModulesByFile(includer) ?? []) {
60
61
  // SAFETY: the module came out of this module graph; `never` only
61
62
  // reflects that the structural slice doesn't model the node type.
62
63
  moduleGraph.invalidateModule(mod as never);
63
64
  }
64
- // Plain `.md` pages have no Vite module: their HTML lives in the
65
- // content-layer store, rendered at sync time. Bump the page's mtime so
66
- // Astro's content watcher re-syncs it — the include-aware digest
67
- // (`withIncludeRefresh`) then forces a fresh render that re-reads the
68
- // edited partial.
69
- try {
70
- // oxlint-disable-next-line no-await-in-loop -- ordered per-page touch
71
- await utimes(includer, now, now);
72
- } catch {
73
- // The page may have been deleted since the graph was written.
74
- }
75
65
  }
66
+ // Plain `.md` pages have no Vite module: their HTML lives in the
67
+ // content-layer store, rendered at sync time. Ask Astro to re-run the
68
+ // loaders — `withIncludeRefresh` then evicts the includers whose partials
69
+ // changed, so the glob loader renders them afresh. `false` only before
70
+ // the server's `astro:server:setup` has run, when there is no store to go
71
+ // stale yet.
72
+ await refreshBlumeContent();
76
73
  ws.send({ type: "full-reload" });
77
74
  // The partial itself is not a module; suppress Vite's default handling.
78
75
  return [];
Binary file
@@ -3,7 +3,12 @@ export { generateRuntime, prerenderDepsPlugin } from "./generate.ts";
3
3
  export { includeHmrPlugin } from "./include-hmr.ts";
4
4
  export { withIncludeRefresh } from "./include-refresh.ts";
5
5
  export type { GenerateResult } from "./generate.ts";
6
- export { blumeIntegration } from "./integration.ts";
6
+ export {
7
+ blumeIntegration,
8
+ publishBuildProject,
9
+ publishDevNegotiation,
10
+ refreshBlumeContent,
11
+ } from "./integration.ts";
7
12
  export type { BlumeIntegrationOptions, BlumePageRoute } from "./integration.ts";
8
13
  export {
9
14
  publishRuntimeModules,